# Georeferencing the city parking map Turns the City of Sandpoint's printed **Downtown & Waterfront Public Parking** PDF into `app/src/features/citymap/parkingAreas.json` — the colour-coded overlay the app draws and hit-tests against. Run this again when the city publishes a new edition of the map. ## Why it needs doing at all The PDF carries **no** georeferencing metadata (no `/Measure`, `/GPTS`, `/LPTS`, `/GEO`, `/Viewport`, `/GCS`). It is a north-up Web Mercator screenshot of a slippy map with vector parking stripes drawn on top, so page coordinates relate to the world by a plain affine transform — which has to be recovered by fitting the drawing to something we already know the coordinates of. That something is OpenStreetMap's street centrelines. Two structural details cost the most time, so they are worth knowing up front: - `pdftocairo` writes each **stroked** street segment with its own `matrix()` transform and *local* coordinates, while **filled** lots are in absolute page coordinates. Both have to be handled or the streets land in a heap near the origin. - The legend swatches are drawn in the same five colours as the real geometry. They are identified by stroke-width 7 inside the legend card's x-band and dropped. ## Pipeline ```bash # 0. deps: poppler-utils (pdftocairo, pdftotext, pdfimages), python3, curl pdftocairo -svg downtown_and_waterfront_public_parking_map.pdf map.svg # 1. vector geometry -> page coordinates, bucketed by the legend's five colours python3 extract_map.py map.svg map_page_coords.json # 2. OSM street centrelines for downtown Sandpoint curl -s --data-binary @roads.overpass https://overpass-api.de/api/interpreter -o osm.json # 3. fit page -> Web Mercator against named streets; prints per-street residuals python3 georef.py # writes fit_raw.json # 4. apply the fit, name each area from OSM, verify, emit the GeoJSON python3 build_geojson.py # writes parking_areas.geojson cp parking_areas.geojson ../../app/src/features/citymap/parkingAreas.json ``` ## What "good" looks like `georef.py` prints a residual per control street and `build_geojson.py` prints how far each on-street segment sits from the nearest OSM road. The current edition fits to: | Check | Result | | --- | --- | | X control residual (avenues) | **RMS 4.1 m** | | Y control residual (streets) | **RMS 3.5 m** | | On-street segments vs nearest OSM road | **mean 4.0 m**, 40 of 42 under 10 m | The two segments over 10 m (`sp-039`, `sp-040`) are correct, not errors: they are angled bays along the old rail corridor that sit on no named road at all — the nearest way is a service alley 19 m off. Anything much worse than the table above means a control street was mis-identified; `georef.py`'s per-street residuals will say which. Residual error is also correctable after the fact without re-running any of this — the app's **Account → Align city map** screen nudges the whole overlay against a live GPS fix and persists the correction. ## Control points `georef.py` maps page grid lines to OSM street names by hand (`AVENUES` / `STREETS`). Sandpoint's grid **jogs** between its north and south halves — North 2nd Ave and South 2nd Ave are 38 m apart — so each control street is measured only over the span its page segment actually covers, and both halves are used as independent control points. That jog is a useful sanity check: the page shows the same 9.3 pt offset, which at the fitted scale is 38 m.