trips/router/README.md
Greg Pomerantz 0c4a1f9f60 router: working local OSRM backend + per-backend bench goldens
- fix encoded-polyline decoder (zigzag: (n>>1) ^ -(n&1)) — caught by
  the OSRM integration tests (Boston clearance 5681km -> 38km)
- OSRM client: routes[] JSON shape, overview=full for geometry,
  polyline+geojson support
- local OSRM 26.4.1 built from source (no sudo): deps.sh (bzip2, lua
  5.2 with readline stub, oneTBB, boost 1.84 via b2 + hand-rolled
  CMake config files, osmium-tool), setup-osrm.sh (merge PBFs ->
  extract -> partition -> customize -> serve :5000); v26 flag fixes
- bench: tasks.osrm.json goldens; both backends 26/26 PASS and agree
  on all 5 optimized orders (Ladd->Cascade etc.)
- README: profile deltas (Valhalla 332min vs OSRM 410min, same route
  shape), per-backend goldens, verified self-host status
2026-09-06 01:19:37 -04:00

141 lines
6.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# maps/router — route-aware planning primitives
The Go backend for the "search along the route" capability
([DESIGN.md](../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)
```sh
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
```sh
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
```sh
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
`/tmp/osm-build` by default (set `W=` for a durable location).
```sh
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 → Queens NY |
|---|---|
| Valhalla `auto` | 332 min, 531 km |
| OSRM `car` | 410 min, 514 km |
Both avoid Boston (22 km vs 38 km clearance) and both take the
I-93/90/495/290 corridor — same route *shape*, different *clocks*.
Consequences: (a) goldens in `bench/tasks.json` 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.
## 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.