trips/router/README.md
Greg Pomerantz 916f9eb503 destination: 39-84 46th St, Sunnyside (OSM way 241831843); durable data location; profile-sensitivity experiment
- destination changed from generic 'Queens NY' to the real address,
  geocoded offline from the NY PBF (addr:street=46th Street,
  addr:housenumber=39-84, postcode 11104 -> 40.74707,-73.9181)
- OSRM tree moved /tmp/osm-build -> /home/gmp/osm-build (durable);
  scripts default W to $HOME/osm-build; LD_LIBRARY_PATH export for
  the baked-in RPATH; cmake stub paths re-pointed
- profile experiment: profiles/car-us.lua (US-tuned speed table on
  stock car.lua) -> Lincoln->Sunnyside 406 -> 377 min vs Valhalla 327;
  README documents the mechanism (motorway=90 x speed_reduction 0.8
  = 72 km/h effective on untagged US interstates)
- bench goldens re-recorded for both backends with new destination;
  both 26/26 PASS, integration tests pass on both routers
2026-09-06 16:48:51 -04:00

7.3 KiB
Raw Blame History

maps/router — route-aware planning primitives

The Go backend for the "search along the route" capability (DESIGN.md, "Driving trips" section). Everything the LLM would otherwise guess about a trip — route shape, travel times, detour costs — is computed here and returned with provenance.

What's in here

internal/geo/       dependency-free geodesy (haversine, point-to-line)
internal/route/     Router interface + backends
                    - valhalla.go: hosted/self-hosted Valhalla (works today;
                      no geometry on the hosted instance → chord fallback)
                    - osrm.go:     local OSRM (full geometry; scripts/setup-osrm.sh)
internal/plan/      the primitives:
                    - stopcost.go: stop_cost  = route(A,C via B)  route(A,C)
                    - optimize.go: optimize_stops (exhaustive ≤10 stops, greedy above)
                    - corridor.go: corridor = buffer band around the route
cmd/routectl/       manual CLI: route | stopcost | optimize
cmd/bench/          benchmark runner (bench/tasks.json)
bench/tasks.json    5 real NE-corridor tasks with recorded goldens
scripts/setup-osrm.sh  self-host OSRM over the NH+MA+CT+NY extract

Zero third-party Go dependencies (stdlib only).

Try it (hosted Valhalla, no setup)

go run ./cmd/routectl route --from 44.054,-71.650 --to 40.729,-73.966
# Lincoln NH → Queens NY: ~5h32m, 531 km, I-93/90/495/290 (NOT via Boston)

go run ./cmd/routectl stopcost --from 44.054,-71.650 --to 40.729,-73.966 \
  --stop 42.2767,-71.806:60:cascade \
  --stop 43.507,-71.548:50:ladd \
  --stop 43.642,-71.156:120:mtmajor
# cascade   detour= 6 min  (on the I-495 corridor)
# ladd      detour=17 min
# mtmajor   detour=80 min  (off-corridor)

go run ./cmd/routectl optimize --from 44.054,-71.650 --to 40.729,-73.966 --k 2 \
  --stop 42.2767,-71.806:60:cascade --stop 43.507,-71.548:50:ladd \
  --stop 43.902,-71.64:60:plymouth --stop 42.425,-71.68:60:brewsters
# order: ladd -> cascade   detour: 23 min

Tests

go test ./...                                  # unit tests, offline, fast
go test -tags integration ./...                # live router (Valhalla by default)
ROUTER_BACKEND=osrm ROUTER_URL=http://localhost:5000 \
  go test -tags integration ./...              # against local OSRM

The integration tests encode the key regression: the fastest Lincoln→Queens drive stays ≥15 km from Boston (it goes via Worcester, I-90/I-495/I-290). That is exactly the fact an LLM confidently gets wrong.

Benchmark

go run ./cmd/bench                                   # valhalla goldens (bench/tasks.json)
go run ./cmd/bench --tasks bench/tasks.osrm.json     # local-OSRM goldens
 go run ./cmd/bench --record                          # re-record into the file you're running

Tasks are real OD pairs across NH/MA/CT/NY with real POI candidates and dwell times. Goldens are per backend+profile (see profile deltas below): bench/tasks.json carries Valhalla goldens, bench/tasks.osrm.json local-OSRM goldens. Both currently 26 PASS / 0 FAIL, and both routers pick the same optimized orders for all 5 tasks despite their different clocks. Check types:

type meaning
golden_route_min direct route time within a recorded band (±10%)
route_avoids route stays ≥ N km from a point (geometry, or bbox when the backend has none)
stop_free / stop_detour_at_least / stop_detour_band computed detour of a candidate vs an expected band
optimize_order the exhaustive optimum matches the recorded order
crosscheck single multi-waypoint route ≈ matrix-summed prediction (≤ maxPct drift)

Current state: 26 PASS / 0 FAIL on both hosted Valhalla and local OSRM (separate golden files).

Self-hosted OSRM (full geometry, exact corridors) — verified

scripts/setup-osrm.sh is the full, tested path (no sudo, no conan): deps.sh builds bzip2, Lua 5.2, oneTBB, Boost 1.84 (b2 + hand-rolled CMake config files — boost release tarballs ship no CMake support) and osmium-tool; then it builds OSRM 26.4.1, merges the 4 state PBFs (osrm-extract takes one input), extracts (~8 GB peak RAM, ~4 min), partitions, customizes, and serves on :5000. Artifacts live in $HOME/osm-build by default (override with W=).

ROUTER_BACKEND=osrm ROUTER_URL=http://localhost:5000 go test -tags integration ./...
go run ./cmd/routectl route --backend osrm --from 44.054,-71.650 --to 40.729,-73.966
# 6h50m, 514.4 km, 9068 geometry points (exact corridor available)

With OSRM, Route.Geometry is populated, so the corridor is exact (distances to the actual routed polyline) instead of the chord fallback used with the hosted Valhalla. This is what makes search_along_route's spatial pre-filter (PostGIS ST_DWithin on the corridor polygon in the v1 design) sound.

Profile deltas matter (measured, same OD pair)

backend Lincoln NH → 39-84 46th St, Sunnyside
Valhalla auto 327 min, 529 km (97 km/h avg)
OSRM stock car.lua 406 min, 511 km (76 km/h avg)
OSRM US-tuned speed table 377 min, 526 km (84 km/h avg)

All three avoid Boston and take the I-93/90/495/290 corridor — same route shape, different clocks.

Why the clocks differ: the times are the profile's speed assumptions, not measurements. OSRM's stock car.lua lists motorway = 90 km/h and applies a global speed_reduction = 0.8 → untagged motorway miles run at ~72 km/h (45 mph). That's a generic/European-flavored default (90 km/h mirrors the European "unsigned road = 90" convention), and US interstates are maxspeed-untagged in large stretches, so the default rules. Re-extracting the same data with a US-tuned table (profiles/car-us.lua: motorway 135, trunk 130, primary 110, ... through the same 0.8 factor) closes two-thirds of the gap (406 → 377 min) and even changes the route choice (526 km now — faster motorways make longer interstates win). Valhalla's auto is simply calibrated more aggressively (97 km/h average).

Consequences: (a) goldens in the bench files are per backend+profile — re-record (--record) after switching; (b) any user-facing arrival-time math must use one backend consistently per trip and say which; (c) "computed" provenance should carry the profile name, because two computed numbers for the same leg can differ by 2025%.

Design notes

  • The route is computed, never narrated. No function in this module returns a time estimate from model knowledge; every number is router output (computed provenance per DESIGN.md R4).
  • Matrix via pairwise route calls. Valhalla's sources_to_targets caps at 150 km, so the common path is BuildMatrix (pairwise /route). OSRM's Table service is available for the local backend when a single-call matrix matters.
  • optimize_stops semantics: the user asked for k stops ("two hikes"), so the objective minimizes total = detour + dwell + overhead over ordered subsets of exactly k, with fallback (a) to the best feasible smaller set when a time budget is given, then (b) to the best exactly-k ignoring the budget, flagged Feasible=false. "At most k" without a budget degenerates (zero stops always wins).
  • Stop coordinates are POI centroids. The router snaps to the network; a 200 m centroid error moves a 531 km route's detour by seconds, not minutes.