Transfers (airport<->hotel) were the app's weakest surface; this fixes the
long-standing cluster of bugs in one pass:
- saneRoute(): reject OSRM results implausible for the straight-line distance
(extract coverage holes once produced a 9h 'route' for a 5km taxi hop);
applied at every routePts call site
- healTransfers() on load: re-sync transfer endpoints against current hotel +
AIRPORTS table, re-sync stays[] against the day-base hotel objects (the
saved stays[] carried pre-coord-fix locations, and dayBase() resolves the
hotel FROM stays — every return leg was routed to the stale hotel), drop
implausible or endpoint-drifted stored routes so they re-route cleanly,
default a missing mode to car, and recover arrLandingMin from the booked
flight time
- the last-stop->hotel ride is now a first-class editable leg (retLeg): the
rail row opens the route card with drag + walk/taxi toggle, exactly like
stop-to-stop legs; previously auto-routed returns seed it
- startMin now actually tracks landing + drop-off (syncArrivalStart had a
null arrLandingMin guard that silently froze the day's start)
- applySavedPlan re-points day: days becomes a new object tree on restore,
and the current-day reference stayed on the stale base-plan day — the rail
rendered the base plan instead of the saved one until a day-tab click
- setDay enriches the opened day (idempotent), so a day opened from a saved
plan gets its legs (incl. the return leg) routed instead of sitting on
straight-line estimates
Also in this change set: correct LLM model name, keyword-router removal (chat
goes to the LLM), screenshot_map + relocate_stop tools for the UI LLM,
server-side plan store with localStorage migration, thinking toggle +
keep-alive warm-up, three-state LLM guard with proactive model load,
spatial /near /nearest /corridor SQL fixes, and the full data.js
coordinate correction for the Colombia demo.
Root cause: several Cartagena old-city landmarks in the data had latitudes
~4 km too high (≈10.46°N instead of ≈10.42°N), dropping them out into the
Caribbean bay — that's why Day 2 stops 2–5 rendered in the water. The chat
model couldn't fix them because its web search couldn't surface valid
coordinates for those specific buildings, and it (correctly) refused to move
them to arbitrary guessed locations.
Also found the CTG (Rafael Núñez) airport was misplaced ~8 km east onto the
Bocagrande area ([10.4622,-75.4385]); the real airport is [10.4425,-75.5131].
This inflated the Day 1 arrival drop-off to 313 min.
Changes:
- data.js: re-anchor 6 old-city stops (S5, S7–S10, S20) + the C2 candidate to
verified on-land coordinates (all snap <21 m to the OSRM extract); fix the
CTG airport in the CTG→SMR flight stations and the S22 stop.
- app.js: correct AIRPORTS.ctg (used by the arrival transfer) to the real
airport. Add healCoordinates() so a plan saved before this fix (still holding
the old in-the-bay points in the browser's localStorage) is re-anchored to
the corrected source on load — stops re-sync by id, arrival/departure airports
re-derived from the AIRPORTS table. Idempotent; user-swapped stops (candidate
ids) are left alone. Verified with a CDP test that corrupts the saved plan
(6 stops + airport) and confirms a reload self-heals them back.
All regression suites pass: coords 6/6, drop-off leg 17/17, smoke 0 errors,
rail structure 13/13, dates 7/7.
The mock can now work several trips at once: a trip picker in the
topbar, per-trip state in localStorage (edits + version history survive
switching), and a per-trip router key so each trip routes on its own
OSRM extract.
Colombia (Cartagena -> Santa Marta, 8 days) becomes the default working
trip, inside a new walking-profile OSRM instance:
- scripts/crop_pbf.py: streaming 3-pass PBF cropper (no PBF library;
the country file is too big for osmium extract -s complete_ways on
this box). Framing validated against osmium test fixtures and
osrm-extract. Drops relations (foot profile ignores restrictions).
- scripts/setup-osrm-colombia.sh: download -> crop (~4.6 MB) ->
extract/partition/customize with the official foot.lua -> serve
:5003. Verified: Getsemaní -> Castillo San Felipe 7.98 km / 96 min.
- server.js: ROUTERS map, ?router=colombia|northeast on /route,
/table, /router-status (per-router probe points).
- app.js: legs touching a transit stop are fixed (flight/taxi/bus via
M.multimodal), never re-routed; distance-0 router answers (both
points on one node) read as adjacent stops, not out-of-coverage.
- multi-hotel fix: day-scope map fit uses the stays covering THAT day,
so a two-city trip no longer fits the whole country per day.
Also: pre-cache-tiles.sh (Cartagena+Santa Marta z14-16, 2560 tiles),
tile cache write fix (Node rejects flags:'x'; buffer-then-write),
favicon, renderHotelMks load-order guard, tests updated for the new
default trip (all four puppeteer suites green, zero console errors).
- server.js: proxy /route, /table, /router-status to the local OSRM on :5000
- app.js: routeClient (polyline-5 decode, ordered route fetch); legs start as
straight-line estimates (conf 1) and are upgraded in the background to real
road geometry + duration (conf 3); out-of-coverage trips (Florence) fall
back gracefully with a 'estimate — outside router coverage' source
- L3 leg editor: dragging a waypoint now genuinely re-routes through the
router (was silently no-op — #app covered the map with pointer-events:auto,
so no map drag ever landed; #app is now pointer-events:none, children opt in)
- data.js: second demo city (Boston 14–16 Sep, inside the router's NE-US
extract) via ?city=boston; nudges + multimodal legs are now dataset-driven
- L0: Day/Trip scope toggle — per-day summary cards (pacing bar, stops,
end time, price, base) with click-to-jump; map fits the whole trip
- version history: commit() snapshots days+stays on every mutation; topbar
vN pill with undo/redo + dropdown timeline showing a diff label per version;
Ctrl+Z / Ctrl+Shift+Z / Ctrl+Y; restore() re-enriches legs (snapshots may
predate route enrichment)
- offline: staged 'prepare for offline' card in the trip rail (itinerary →
POI details → vector tiles) that flips the topbar badge to '📦 offline ready'
- chat/scripts/nudges: Florence-specific strings generalized (stay name/dates,
vibes, pacing summary) so both demo cities read naturally