Interactive demo of route optimization for courier deliveries. It scatters a batch of delivery points across Madrid, runs several TSP heuristics over them, and shows how much each strategy saves compared to visiting the points in the order they came in. Click a row in the comparison table to see that route on the map.
Live at isistomin.com/tsp.
This is a public rebuild of a routing problem I worked on for a courier logistics bot: given a day's delivery list, produce a sane visiting order fast enough to not matter. Exact TSP solvers are overkill at this scale — a greedy constructor plus 2-opt refinement lands within a few percent of optimal in under a millisecond for 15–25 stops.
All strategies produce a closed tour and are implemented as pure functions in
src/tsp.ts. Distances are great-circle (haversine), in
kilometres.
Naive — visit points in generation order. Not an algorithm, just the baseline every other strategy is measured against.
Nearest neighbour — start at the first point and repeatedly hop to the closest unvisited one. O(n²), trivially simple, and usually already 10–20% better than the baseline. Its weakness is the endgame: the last few unvisited points tend to be scattered leftovers, and the tour pays for them with long closing edges.
Greedy edge — instead of growing a single path, sort all point-to-point edges by length and keep taking the shortest one that is still legal: no point may get a third neighbour, and no cycle may close before it spans every point (tracked with a union-find). The kept edges form one Hamiltonian cycle. It avoids nearest neighbour's endgame problem because short edges are claimed globally, not from the perspective of a single walker.
Genetic algorithm — a population of candidate tours evolves for a fixed number of generations: tournament selection picks parents, order crossover (OX1) recombines them while keeping every stop exactly once, and an occasional segment inversion mutates the offspring. The best tour survives each generation unchanged, and the initial population is seeded with the nearest-neighbour tour, so the result never regresses below that baseline. Much more machinery than the greedy constructors for a similar outcome at this problem size — which is rather the point of showing it.
Ant colony — an elitist ant system. Each iteration a colony of ants builds
tours edge by edge, picking the next stop with probability proportional to
pheromone^α · (1/distance)^β. Pheromone then evaporates, every ant deposits
an amount inversely proportional to its tour length, and the best tour found
so far gets an extra deposit. Over iterations the pheromone trail concentrates
on short edges and the colony converges. Stochastic like the GA, and the most
expensive strategy in the table, but it explores the search space in a
qualitatively different way: through a shared memory of good edges rather
than recombination of whole tours.
2-opt — a refinement pass over an existing tour, not a constructor. While any two edges (a→b, c→d) can be replaced by (a→c, b→d) for a net saving, reverse the segment between them and repeat. This uncrosses the route until it reaches a local minimum (with an iteration cap as a safety net). Applied here on top of both constructors; the refined variants are consistently the shortest, typically 15–30% below the baseline.
npm ci
npm run devOther scripts: npm test (vitest over the TSP core), npm run lint,
npm run typecheck, npm run build (emits dist/).
The build is plain static files rooted at /tsp/ (base in
vite.config.ts), served by the nginx container of
isistomin.com via a bind mount. The build itself
also runs in Docker (see docker-compose.yml), so the
server needs no Node installation.
On the server:
git clone https://github.com/isistomin/tsp.git /root/tsp
cd /root/tsp && docker compose run --rm buildIn the site's docker-compose.yml, mount the build into the nginx container:
volumes:
- /root/tsp/dist:/var/www/tsp:roIn the HTTPS server block:
location = /tsp { return 301 /tsp/; }
location /tsp/ {
alias /var/www/tsp/;
index index.html;
}Recreate the container so the new volume is picked up (reload is not
enough): docker compose up -d --force-recreate nginx. Verify with
curl -sI https://isistomin.com/tsp/ | head -3.
Updating the demo afterwards is just git pull && docker compose run --rm build
in the repo — nginx serves the new files directly, no restarts.