Archive osm-overlays change, sync 4 specs to main

Synced to main specs:
- osm-poi-overlays (new): POI overlay requirements
- osm-tile-overlays (new): tile overlay requirements
- map-display (updated): added overlay layer entries
- planner-session (updated): added overlay/POI sync

Archived to openspec/changes/archive/2026-04-11-osm-overlays/.
All 4 artifacts complete. All 45 tasks complete.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ullrich Schäfer 2026-04-11 02:45:32 +02:00
parent f4ebf75a42
commit 7dc4fdec3b
No known key found for this signature in database
GPG key ID: A32FF691A0F752D9
12 changed files with 179 additions and 98 deletions

View file

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-03-28

View file

@ -0,0 +1,214 @@
## Context
The Planner currently has three base tile layers (OSM, OpenTopoMap, CyclOSM) in
`packages/map/src/layers.ts`, rendered via Leaflet's `LayersControl`. There are
no overlay layers and no POI display. brouter-web offers hillshading, Waymarked
Trails networks, and ~60 Overpass-powered POI categories — a model worth
adopting selectively.
The Planner is stateless (Yjs CRDT), collaborative, and uses BRouter for
routing. Overlay state should sync across participants.
## Goals / Non-Goals
**Goals:**
- Add tile-based overlays (hillshading, waymarked trails) to the layer switcher
- Add viewport-scoped POI overlays from Overpass API with per-category toggling
- Auto-suggest relevant overlays based on routing profile
- Build a reusable Overpass client shared with waypoint-notes POI snap
**Non-Goals:**
- Custom tile server or self-hosted overlays (use public tile services)
- Full brouter-web layer catalog (50+ layers, most country-specific)
- Offline/cached tile data
- Vector tile overlays (MVT) — stick with raster for now
- POI editing or contributing back to OSM
## Decisions
### D1: Tile overlay definitions
Add an `overlayLayers` export to `packages/map/src/layers.ts` alongside
existing `baseLayers`. Each overlay is a transparent tile layer rendered on top
of the base layer.
Initial overlays:
| Id | Name | URL | Attribution |
|----|------|-----|-------------|
| `hillshading` | Hillshading | `https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png` via [Leaflet.TileLayer.Terrarium](https://github.com/pka/leaflet-terrarium-hillshading) or pre-rendered from `https://tiles.wmflabs.org/hillshading/{z}/{x}/{y}.png` | SRTM/Mapzen |
| `waymarked-cycling` | Cycling Routes | `https://tile.waymarkedtrails.org/cycling/{z}/{x}/{y}.png` | Waymarked Trails (CC-BY-SA) |
| `waymarked-hiking` | Hiking Routes | `https://tile.waymarkedtrails.org/hiking/{z}/{x}/{y}.png` | Waymarked Trails (CC-BY-SA) |
| `waymarked-mtb` | MTB Routes | `https://tile.waymarkedtrails.org/mtb/{z}/{x}/{y}.png` | Waymarked Trails (CC-BY-SA) |
These are all free, public tile endpoints used by brouter-web and other OSM
tools. No API keys needed.
**Alternative considered**: Thunderforest Outdoors or OpenCycleMap — requires
API key, limited free tier. Not worth the complexity.
### D2: Overlay toggle in LayersControl
Leaflet's `LayersControl` already supports overlays natively via
`LayersControl.Overlay` (react-leaflet). Add overlay tile layers as checkboxes
alongside the existing base layer radio buttons. No custom UI needed for Phase 1.
### D3: POI category system
Define POI categories as a typed configuration mapping OSM tags to display
properties. Inspired by brouter-web's `layers/overpass/` structure but
simplified to the categories most relevant for route planning:
```typescript
interface PoiCategory {
id: string;
name: string; // i18n key
icon: string; // emoji or SVG icon id
color: string; // marker color
query: string; // Overpass QL fragment, e.g. "nwr[amenity=drinking_water]"
profiles?: string[]; // routing profiles where this is auto-enabled
}
```
**Initial categories** (curated from brouter-web's full list):
| Category | POI types (OSM tags) | Icon | Auto-enable for |
|----------|---------------------|------|-----------------|
| Drinking water | `amenity=drinking_water`, `amenity=water_point` | 💧 | all |
| Shelter | `amenity=shelter`, `tourism=wilderness_hut` | 🛖 | hiking |
| Camping | `tourism=camp_site`, `tourism=caravan_site`, `tourism=picnic_site` | ⛺ | all |
| Food & drink | `amenity=restaurant`, `amenity=cafe`, `amenity=fast_food`, `amenity=pub`, `amenity=biergarten` | 🍽️ | — |
| Groceries | `shop=supermarket`, `shop=convenience`, `shop=bakery` | 🛒 | — |
| Bike infrastructure | `amenity=bicycle_parking`, `amenity=bicycle_repair_station`, `amenity=bicycle_rental` | 🔧 | cycling |
| Accommodation | `tourism=hotel`, `tourism=hostel`, `tourism=guest_house` | 🏨 | — |
| Viewpoints | `tourism=viewpoint` | 👁️ | hiking |
| Toilets | `amenity=toilets` | 🚻 | — |
**Not included** (from brouter-web but too niche): ATMs, banks, benches,
telephones, kneipp water cures, car parking, railway stations, art galleries,
museums, ice cream shops, BBQs. Can be added later by extending the config.
### D4: Overpass client
Create `apps/planner/app/lib/overpass.ts` with:
- `queryPois(bbox, categories): Promise<Poi[]>` — builds Overpass QL query
combining all enabled categories into one request (union query), returns
parsed GeoJSON features
- **Bbox query**: `[bbox:south,west,north,east]` in Overpass QL, scoped to
current Leaflet viewport
- **Endpoint**: `https://overpass-api.de/api/interpreter` (public, no key)
- **Response format**: Request `[out:json]` for easier parsing than XML
- **Deduplication**: Overpass may return same node via multiple tags — dedup by
OSM id
This client is also used by the waypoint-notes POI snap feature (smaller radius
query around a single waypoint).
### D5: POI caching and rate limiting
Overpass API is public and rate-limited. Must be respectful:
- **Debounce**: 500ms after map `moveend` before querying
- **Abort**: Cancel in-flight requests when viewport changes (AbortController)
- **Tile-based caching**: Quantize viewport to grid tiles (e.g., 0.1° cells),
cache results per tile. Reuse cached tiles that overlap new viewport.
- **TTL**: 10 minutes for cached tiles (POI data changes slowly)
- **Max concurrent**: 1 request at a time
- **429 handling**: Exponential backoff, disable auto-refresh temporarily, show
"POI data unavailable" message
- **Zoom threshold**: Only query POIs at zoom >= 12 (avoids massive result sets
at country-level zoom)
### D6: POI overlay panel
A collapsible panel (not the LayersControl — too many items) for toggling POI
categories. Positioned below the layer switcher on the right side of the map.
- Toggle button with POI icon to open/close
- Checkbox per category with icon and name
- "Loading..." indicator while Overpass query is in flight
- Category count badge showing number of visible POIs
- Panel state (which categories are enabled) synced via Yjs so all participants
see the same POIs
### D7: POI marker rendering
- Use Leaflet `L.Marker` with `L.DivIcon` for each POI (not CircleMarker —
need icons)
- Icon shows the category emoji/icon at 20×20px
- Popup on click showing: name, category, opening hours (if available), website
link (if available), OSM link
- **Clustering**: Use `leaflet.markercluster` at low zoom levels to avoid
thousands of markers. Cluster by category color.
- **Z-index**: POI markers below route and waypoint markers
### D8: Profile-aware overlay defaults
When the routing profile changes (via Yjs `routeOptions.profile`), suggest
relevant overlays:
- **Cycling profiles** (cycling-safe, cycling-fast, etc.): Auto-enable
Waymarked Cycling overlay + Bike infrastructure POIs
- **Hiking profiles**: Auto-enable Waymarked Hiking + Shelter + Viewpoints
- **MTB profiles**: Auto-enable Waymarked MTB + Bike infrastructure
"Auto-enable" means toggling on when profile changes, not forcing — users can
still disable. Only auto-enable on profile change, not on page load (respect
user's previous choice stored in Yjs).
### D9: Yjs overlay state
Store enabled overlays in Yjs `routeOptions` Y.Map:
```
routeOptions.overlays = ["hillshading", "waymarked-cycling"]
routeOptions.poiCategories = ["drinking_water", "camping", "bike_infra"]
```
Array of string IDs. Changes sync to all participants. Persisted in crash
recovery localStorage snapshot.
### D10: POI-to-waypoint integration
POIs can become waypoints through three paths, all using the same snap logic:
1. **"Add as waypoint" button** in POI popup → appends to end of route
2. **Click near a POI** (within 50m) → new waypoint snaps to POI position
3. **Drag waypoint near a POI** → snaps on drop, clears name if dragged away
When snapping, the waypoint's Yjs Y.Map stores:
- `osmId`: OSM node ID for future cross-referencing
- `poiTags`: Key tags (phone, address, website, opening hours, amenity type)
This metadata persists through Yjs and will be available for Journal display.
### D11: Overpass endpoint fallback
Primary endpoint is `overpass.kumi.systems` (higher rate limits, same as
brouter-web). Falls back to `overpass-api.de` if the primary fails. The
Overpass API sometimes returns rate limit errors as HTTP 200 with
"rate_limited" in the body — the client checks for this pattern.
### D12: Z-index layering
Marker z-index offsets centralized in `apps/planner/app/lib/z-index.ts`:
- Cursors (-1000) < Ghost waypoint (-100) < Waypoints (1000) <
Waypoint highlighted (1200) < POI markers (1500) < Highlight dot (2000)
## Risks / Trade-offs
- **Overpass API availability**: Public endpoint, no SLA. If down, POI overlays
fail gracefully (show message, tile overlays still work). Fallback endpoint
(`overpass.kumi.systems``overpass-api.de`) implemented.
- **Tile service availability**: Waymarked Trails and hillshading tiles are
community-run. → Degrade gracefully if tiles 404. Consider self-hosting tiles
if usage grows.
- **Performance with many POIs**: Dense areas (cities) may return hundreds of
POIs. → Zoom threshold (>=10) + 100 element limit mitigate this. Marker
clustering deferred — can add later if density becomes an issue.
- **Overpass query cost**: Combining many categories into one query is efficient
but returns large payloads. → Only query enabled categories, not all. 1MB
maxsize limit, 10s timeout.
- **Routing rate limit**: Multi-waypoint routes with POI snapping can trigger
rapid recomputes. Increased rate limit from 60 to 300/hour to accommodate.

View file

@ -0,0 +1,30 @@
## Why
The Planner shows three base tile layers (OSM, OpenTopoMap, CyclOSM) but no overlays. Route planners like brouter-web offer hillshading, waymarked trail networks, and toggleable POI layers from OpenStreetMap — information that is essential for planning multi-day bike tours and hikes. Without overlays, users must cross-reference other tools to find water sources, campsites, bike repair stations, or see official trail routes.
## What Changes
- **Tile overlays**: Add hillshading and Waymarked Trails (cycling, hiking, MTB) as toggle-able overlay layers in the Leaflet LayersControl
- **POI overlay panel**: Add a collapsible panel to toggle categories of OSM points of interest (water, shelter, camping, food, bike infrastructure, accommodation) queried from the Overpass API within the current viewport
- **POI markers**: Render POI results as categorized markers with icons, name labels, and popups showing OSM tags (opening hours, website, etc.)
- **Profile-aware defaults**: Auto-enable relevant overlays based on the active routing profile (cycling → Waymarked Cycling + bike POIs; hiking → Waymarked Hiking + water/shelter POIs)
- **Overpass client**: Shared utility for querying the Overpass API with caching, debouncing, rate limit handling, and endpoint fallback (kumi.systems → overpass-api.de)
- **POI → waypoint**: "Add as waypoint" button in POI popups, plus automatic snapping of placed/dragged waypoints to nearby POIs (50m threshold) with OSM metadata preserved
- **POI metadata on waypoints**: Waypoints snapped to POIs store osmId and key tags (phone, address, website, opening hours) for future Journal display
## Capabilities
### New Capabilities
- `osm-tile-overlays`: Hillshading and Waymarked Trails tile overlays in the map layer switcher
- `osm-poi-overlays`: Toggleable POI categories from Overpass API rendered as map markers within the viewport
### Modified Capabilities
- `map-display`: Add overlay layers to the layer switcher alongside existing base layers
- `planner-session`: Persist enabled overlay state in Yjs so all participants see the same overlays
## Impact
- **Files**: `packages/map/src/layers.ts` (overlay tile definitions), new POI overlay components in `apps/planner/`, new Overpass client in `packages/map/` or `apps/planner/app/lib/`
- **APIs**: Overpass API (external, rate-limited — public endpoint at `overpass-api.de`)
- **Dependencies**: None new — Leaflet handles tile overlays natively, Overpass is a REST API
- **Performance**: Tile overlays are lightweight (transparent PNGs). POI overlays need viewport-scoped queries with caching to avoid excessive Overpass load.

View file

@ -0,0 +1,20 @@
## MODIFIED Requirements
### Requirement: Base layer switching
The map SHALL support switching between multiple base tile layers, and SHALL support toggling overlay tile layers independently.
#### Scenario: Switch to OpenTopoMap
- **WHEN** a user selects "OpenTopoMap" from the layer switcher
- **THEN** the map tiles change to topographic tiles from OpenTopoMap
#### Scenario: Available base layers
- **WHEN** a user opens the layer switcher
- **THEN** the options include OpenStreetMap, OpenTopoMap, and CyclOSM
#### Scenario: Available overlay layers
- **WHEN** a user opens the layer switcher
- **THEN** overlay checkboxes are shown for Hillshading, Cycling Routes, Hiking Routes, and MTB Routes
#### Scenario: Toggle overlay
- **WHEN** a user checks an overlay checkbox in the layer switcher
- **THEN** the overlay tiles are rendered on top of the current base layer

View file

@ -0,0 +1,83 @@
## ADDED Requirements
### Requirement: POI overlay panel
The Planner SHALL provide a collapsible panel for toggling POI categories on the map.
#### Scenario: Open POI panel
- **WHEN** a user clicks the POI toggle button on the map
- **THEN** a panel opens showing checkboxes for each POI category with icons and names
#### Scenario: Close POI panel
- **WHEN** the POI panel is open and the user clicks the toggle button again
- **THEN** the panel collapses and POI markers remain visible on the map
### Requirement: POI categories
The Planner SHALL support the following POI categories queried from OpenStreetMap via Overpass API: drinking water, shelter, camping, food & drink, groceries, bike infrastructure, accommodation, viewpoints, and toilets.
#### Scenario: Enable a POI category
- **WHEN** a user enables the "Drinking water" category in the POI panel
- **THEN** drinking water POIs within the current map viewport are fetched from Overpass and rendered as markers
#### Scenario: Disable a POI category
- **WHEN** a user disables a previously enabled POI category
- **THEN** markers for that category are removed from the map
#### Scenario: Multiple categories enabled
- **WHEN** a user enables "Camping" and "Drinking water" simultaneously
- **THEN** both categories of markers are visible, each with distinct icons
### Requirement: POI markers
Each POI SHALL be rendered as a map marker with a category-specific icon.
#### Scenario: POI marker display
- **WHEN** POIs are loaded for an enabled category
- **THEN** each POI appears as a small icon marker at its coordinates on the map
#### Scenario: POI marker popup
- **WHEN** a user clicks a POI marker
- **THEN** a popup shows the POI name, category, and available details (opening hours, website, OSM link)
#### Scenario: POI marker clustering
- **WHEN** many POIs are visible in a small area
- **THEN** markers are clustered with a count badge, and expand when the user zooms in
### Requirement: Viewport-scoped POI loading
The Planner SHALL load POIs only within the current map viewport, refreshing when the viewport changes.
#### Scenario: Load POIs on viewport
- **WHEN** POI categories are enabled and the user pans or zooms the map
- **THEN** POIs are fetched for the new viewport after a 500ms debounce
#### Scenario: Zoom threshold
- **WHEN** the map zoom level is below 12
- **THEN** POI queries are not sent and a message indicates the user should zoom in to see POIs
#### Scenario: Cached results
- **WHEN** the user pans back to a previously viewed area within 10 minutes
- **THEN** cached POI results are displayed without a new Overpass query
### Requirement: Overpass rate limit handling
The Planner SHALL handle Overpass API rate limits gracefully.
#### Scenario: Rate limited response
- **WHEN** the Overpass API returns a 429 status
- **THEN** the Planner shows a temporary "POI data unavailable — try again shortly" message and retries with exponential backoff
#### Scenario: Overpass unavailable
- **WHEN** the Overpass API is unreachable
- **THEN** the Planner shows a message and tile overlays continue to function normally
### Requirement: Profile-aware POI defaults
The Planner SHALL auto-enable relevant POI categories when the routing profile changes.
#### Scenario: Cycling profile POI defaults
- **WHEN** the routing profile is changed to a cycling variant
- **THEN** the "Bike infrastructure" POI category is automatically enabled
#### Scenario: Hiking profile POI defaults
- **WHEN** the routing profile is changed to a hiking variant
- **THEN** "Shelter" and "Viewpoints" POI categories are automatically enabled
#### Scenario: User override persists
- **WHEN** a user manually disables an auto-enabled POI category
- **THEN** it remains disabled until the next profile change

View file

@ -0,0 +1,68 @@
## ADDED Requirements
### Requirement: Hillshading overlay
The Planner map SHALL offer a hillshading tile overlay that visualizes terrain relief.
#### Scenario: Enable hillshading
- **WHEN** a user toggles "Hillshading" in the layer switcher
- **THEN** semi-transparent terrain shading tiles are rendered on top of the base layer
#### Scenario: Hillshading with any base layer
- **WHEN** hillshading is enabled and the user switches base layers
- **THEN** hillshading remains visible on top of the new base layer
### Requirement: Waymarked Trails cycling overlay
The Planner map SHALL offer a Waymarked Trails cycling overlay showing official cycle route networks.
#### Scenario: Enable cycling routes overlay
- **WHEN** a user toggles "Cycling Routes" in the layer switcher
- **THEN** official cycling routes (EuroVelo, national networks) are rendered as colored lines on the map from Waymarked Trails tiles
#### Scenario: Cycling overlay at different zoom levels
- **WHEN** cycling routes overlay is enabled
- **THEN** international routes are visible at low zoom and local routes appear at higher zoom levels
### Requirement: Waymarked Trails hiking overlay
The Planner map SHALL offer a Waymarked Trails hiking overlay showing official hiking trail networks.
#### Scenario: Enable hiking routes overlay
- **WHEN** a user toggles "Hiking Routes" in the layer switcher
- **THEN** official hiking trails (GR routes, national trails) are rendered as colored lines on the map
### Requirement: Waymarked Trails MTB overlay
The Planner map SHALL offer a Waymarked Trails MTB overlay showing official mountain bike trail networks.
#### Scenario: Enable MTB routes overlay
- **WHEN** a user toggles "MTB Routes" in the layer switcher
- **THEN** official MTB trails are rendered as colored lines on the map
### Requirement: Multiple simultaneous overlays
The Planner map SHALL support enabling multiple tile overlays at the same time.
#### Scenario: Hillshading plus cycling routes
- **WHEN** a user enables both "Hillshading" and "Cycling Routes"
- **THEN** both overlays are visible simultaneously, with cycling routes rendered above hillshading
### Requirement: Overlay tile attribution
Each tile overlay SHALL display proper attribution when enabled.
#### Scenario: Attribution updates
- **WHEN** an overlay is toggled on
- **THEN** its attribution text is added to the map attribution control
- **WHEN** the overlay is toggled off
- **THEN** its attribution text is removed
### Requirement: Profile-aware overlay suggestions
The Planner SHALL auto-enable relevant tile overlays when the routing profile changes.
#### Scenario: Switch to cycling profile
- **WHEN** the routing profile is changed to a cycling variant
- **THEN** the Waymarked Trails cycling overlay is automatically enabled
#### Scenario: Switch to hiking profile
- **WHEN** the routing profile is changed to a hiking variant
- **THEN** the Waymarked Trails hiking overlay is automatically enabled
#### Scenario: User can disable auto-enabled overlays
- **WHEN** an overlay was auto-enabled by a profile change
- **THEN** the user can manually disable it and it stays disabled until the next profile change

View file

@ -0,0 +1,24 @@
## MODIFIED Requirements
### Requirement: Real-time collaborative editing
The Planner SHALL synchronize waypoint edits, route options, and overlay preferences across all connected participants in real-time using Yjs CRDTs.
#### Scenario: Add waypoint
- **WHEN** participant A adds a waypoint to the map
- **THEN** participant B sees the waypoint appear within 500ms
#### Scenario: Reorder waypoints
- **WHEN** participant A drags a waypoint to reorder it
- **THEN** participant B sees the updated waypoint order within 500ms
#### Scenario: Concurrent edits
- **WHEN** participant A and B both add waypoints simultaneously
- **THEN** both waypoints appear for both participants without conflict
#### Scenario: Overlay sync
- **WHEN** participant A enables the "Hillshading" tile overlay
- **THEN** participant B sees hillshading appear on their map within 500ms
#### Scenario: POI category sync
- **WHEN** participant A enables the "Drinking water" POI category
- **THEN** participant B sees drinking water markers appear on their map

View file

@ -0,0 +1,80 @@
## 1. Tile Overlay Definitions
- [x] 1.1 Add `overlayLayers` export to `packages/map/src/layers.ts` with hillshading, Waymarked Cycling, Waymarked Hiking, Waymarked MTB tile configs
- [x] 1.2 Add `LayersControl.Overlay` entries in `MapView.tsx` and `PlannerMap.tsx` for each overlay
- [x] 1.3 Verify overlay attribution updates correctly when toggling overlays on/off
## 2. Overlay State Sync
- [x] 2.1 Add `overlays` string array to Yjs `routeOptions` Y.Map for enabled tile overlay IDs
- [x] 2.2 Add `poiCategories` string array to Yjs `routeOptions` Y.Map for enabled POI category IDs
- [x] 2.3 Sync LayersControl state with Yjs — toggling overlay updates Yjs, Yjs changes toggle layers
- [x] 2.4 Include overlay state in crash recovery localStorage snapshot
## 3. Overpass Client
- [x] 3.1 Create `apps/planner/app/lib/overpass.ts` with `queryPois(bbox, categories)` function
- [x] 3.2 Build Overpass QL union queries from enabled POI category configs
- [x] 3.3 Parse `[out:json]` response into typed `Poi` objects (id, lat, lon, name, category, tags)
- [x] 3.4 Deduplicate results by OSM node ID (same node may match multiple tag queries)
## 4. POI Caching & Rate Limiting
- [x] 4.1 Implement tile-based cache: quantize viewport to 0.1° grid cells, cache per cell with 10-minute TTL
- [x] 4.2 Add 500ms debounce on map `moveend` before triggering Overpass query
- [x] 4.3 Use AbortController to cancel in-flight requests when viewport changes
- [x] 4.4 Handle 429 responses with exponential backoff and user-visible message
- [x] 4.5 Enforce zoom >= 12 threshold — show "Zoom in to see POIs" message below
## 5. POI Category Configuration
- [x] 5.1 Define `PoiCategory` type and initial category configs (water, shelter, camping, food, groceries, bike infra, accommodation, viewpoints, toilets)
- [x] 5.2 Map each category to Overpass QL tag queries, icon, color, and applicable routing profiles
## 6. POI Overlay Panel
- [x] 6.1 Create collapsible POI panel component with toggle button (map right side, below layer switcher)
- [x] 6.2 Render checkbox per POI category with icon, name, and visible count badge
- [x] 6.3 Show loading indicator while Overpass query is in flight
- [x] 6.4 Show empty/error states (no results, Overpass unavailable, zoom too low)
## 7. POI Marker Rendering
- [x] 7.1 Render POI markers using `L.Marker` with `L.DivIcon` showing category icon
- [x] 7.2 Add click popup with POI name, category, opening hours, website, and OSM link
- [x] 7.3 Add `leaflet.markercluster` for clustering dense POI areas (dynamic import to avoid bundle bloat)
- [x] 7.4 Set z-index so POI markers render below route polyline and waypoint markers
## 8. Profile-Aware Defaults
- [x] 8.1 Define profile-to-overlay mapping (cycling → waymarked-cycling + bike POIs, hiking → waymarked-hiking + shelter + viewpoints, MTB → waymarked-mtb + bike POIs)
- [x] 8.2 Auto-enable mapped overlays on routing profile change (update Yjs arrays)
- [x] 8.3 Only auto-enable on explicit profile change, not on initial page load (respect existing Yjs state)
## 9. i18n
- [x] 9.1 Add translation keys for all overlay names, POI category names, and UI strings (en + de)
## 10. Testing
- [x] 10.1 Unit tests for Overpass client: query building, response parsing, deduplication
- [x] 10.2 Unit tests for POI cache: tile quantization, TTL expiry, cache hit/miss
- [x] 10.3 Unit tests for profile-to-overlay mapping
- [x] 10.4 E2E test: enable hillshading overlay, verify tile requests
- [x] 10.5 E2E test: enable POI category, verify markers appear (mock Overpass response)
## 11. POI-Waypoint Integration
- [x] 11.1 Add "Add as waypoint" button to POI popup — appends POI location + name as waypoint
- [x] 11.2 Snap click-to-add waypoints to nearby POIs (50m threshold) with name + metadata
- [x] 11.3 Snap dragged waypoints to nearby POIs, clear name/metadata when dragged away
- [x] 11.4 Snap route-inserted waypoints to nearby POIs
- [x] 11.5 Store osmId and poiTags on Yjs waypoint Y.Map for snapped POIs
## 12. Resilience
- [x] 12.1 Fallback Overpass endpoint (kumi.systems → overpass-api.de)
- [x] 12.2 Handle Overpass rate limit returned as HTTP 200 with error body
- [x] 12.3 Reduce query size (100 results, 1MB maxsize, 10s timeout)
- [x] 12.4 Extract z-index constants into z-index.ts for consistent marker layering
- [x] 12.5 Increase Planner route rate limit from 60 to 300 requests/hour