Wire the map-core codec into route-data.ts so the Yjs session doc stores
the computed route compactly, without changing the routing-host
compute-once-and-share model.
- Codec: add encodeElevations/decodeElevations (delta+varint) so geometry
= encoded [lon,lat] polyline + elevation channel; precision bumped to
1e6 (~0.11 m) to preserve the router's 6-decimal output.
- writeComputedRoute: geometry stored once (encoded polyline + elevations,
no redundant geojson), road metadata run-length encoded.
- Dual-format reads: getCoordinates / readRoadMetadata / new readGeojson
transparently handle the new encoding AND legacy JSON docs (+ oldest
geojson-only) — no migration, no flag day; legacy docs re-encode on the
next recompute. use-elevation-data reads readGeojson (assembled for new
docs).
- Spec correction: a lossy compact codec can't be byte-identical to legacy
(generateGpx emits raw precision), so "GPX byte-compatible" is corrected
to "coordinates preserved within the router's ~0.1 m precision".
Tests: legacy-format read, size assertion (>4x smaller / round-trips),
elevation round-trip. map-core + planner typecheck/lint clean; full
pnpm test 11/11.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Task group 1 of planner-route-encoding. Framework-free `route-codec.ts`:
- encodePolyline/decodePolyline — fixed-precision (1e5) delta + zig-zag
varint + base64; lossless to ~1 m, version-tagged ("p1:").
- encodeRuns/decodeRuns — run-length codec for the per-coordinate
road-metadata channels, tagged ("r1:").
- isEncodedPolyline/isEncodedRuns — so the read path (group 3) tells the
new encoding apart from legacy JSON.
Not yet wired into route-data.ts (groups 2–3). Verified: map-core
typecheck + lint clean, 52/52 tests (round-trip within 1 m, empty/single,
RLE collapse, >3x smaller than JSON).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- poi-extract.sh: PBF download -> osmium tags-filter -> centroid NDJSON +
manifest, published via the Caddy sidecar at vSwitch-only /poi/*
- osmium-filters.txt + poi-selectors.sql generated from map-core selectors,
guarded by sync tests so poiCategories stays the single source of truth
- poi-import.sh: checksum verify -> classify into category rows ->
70% guard (--bootstrap override) -> atomic rename swap; node_exporter
textfile metric for import outcome
- systemd service+timer units for both halves (monthly, offset)
- cd-infra.yml: ship infrastructure/scripts to the flagship
- e2e: mock /api/pois instead of /api/overpass
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the planner's Overpass proxy with a self-hosted POI index:
- map-core POI categories become structured tag selectors (single source of
truth) + osmium filter / classification helpers
- planner.pois PostGIS table (centroid points, GiST + category indexes)
- /api/pois serving route (session + same-origin + rate limit, 100 cap)
- lib/pois.ts client (renamed from overpass.ts; GET, quantized bbox)
- metrics: poi_api_requests_total + DB-collected index rows/age gauges;
drop overpass_* metrics
- remove /api/overpass proxy + dead OVERPASS_URLS on the planner service
(Journal surface backfill still uses it via code default)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Synchronous path + rendering for the surface/waytype breakdown:
- map-core `computeSurfaceBreakdown(coords, surfaces, highways)` → distance-
weighted metres per surface + waytype category (unit-tested);
- `SurfaceBreakdownSchema` in @trails-cool/api;
- nullable `surfaceBreakdown` jsonb on routes + activities;
- Planner `SaveToJournalButton` computes it from the BRouter waytags already in
routeData and sends it; `api.save-to-journal` forwards it; the journal route
callback validates + persists (journal is the authoritative validator);
- `SurfaceBreakdown` component (stacked bars per dimension, map-core palettes,
legend category · % · km largest-first, unknown → "other", hidden when empty)
on route + activity detail; journal gains a @trails-cool/map-core dep;
- i18n journal.surface.* (en + de).
Phase 2 (async Overpass backfill + SSE for imports/uploads, and the e2e that
seeds a breakdown) follows in a separate PR.
Tests: computeSurfaceBreakdown unit (map-core 36); SurfaceBreakdown component
(jsdom, journal 326). typecheck + lint green; verified in the browser.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New renderer-agnostic package with zero dependencies:
- Tile configs (base layers, overlay layers)
- Color palettes (surface, highway, smoothness, tracktype, cycleway,
bikeroute, elevation, maxspeed) — 8 color maps + 3 color functions
- POI category definitions (9 categories with Overpass queries, icons,
colors, profile mappings)
- Z-index layering constants
- Snap distance constant
Updated 15 consuming files to import from @trails-cool/map-core.
Deleted poi-categories.ts and z-index.ts from the Planner (fully moved).
packages/map re-exports tile configs from map-core for backwards compat.
All 117 tests pass, no user-facing changes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>