Repair the three layers a walking score has to stand on - #93
Merged
Merged
Conversation
Groundwork for the walkability view. Three inputs it needs turned out to be broken or missing, and each one would have made the score lie rather than merely fall short, so they are repaired on their own before anything is built on top of them. Pedestrian crossings synced nothing. The Overpass query ended `out tags`, whose verbosity prints ids and tags without node coordinates, so every crossing was discarded by the element converter before it could be stored. The layer reported success and held an empty set. It now asks for geometry, and a new normaliser gives each crossing a stable reading of whether it is signalised, marked, dropped-kerb and tactile — three-state throughout, so a crossing surveyed as unmarked stays distinct from one nobody has looked at. Speed limits were unreadable outside the countries that tag a bare number in km/h. `maxspeed` had no normaliser at all, so `20 mph` and `DE:urban` reached every consumer as raw strings and drew as unknown. They are now parsed once into a comparable figure. Two cases deliberately carry no number: an unrestricted road, because 0 km/h would rank an autobahn as the calmest street in town, and a national zone, because what `urban` means is a question of local law — the zone is preserved and the workspace supplies the value, rather than a table of one country's defaults being baked into core code. A street whose pavement is mapped separately was recorded as having none. `sidewalk=separate` says the footway exists and is drawn as its own line; it was in the set of values meaning "surveyed, nothing here". Left alone, the walking score would have condemned exactly the cities that map pedestrian space best. It now reads as a pointer — `footway_present: null` with `sides: "separate"` — and outranks a `no` on the opposite side, because one side mapped separately still means there is a pavement to find. Also: kissing gates and stiles now report `affects: "walking"`, a value the area-targets document has described since it was written but which nothing ever emitted; and the map's speed colouring reads the new numeric field, keeping the old string match behind a `coalesce` guard so workspaces synced before this change render exactly as they do today. Verified: ruff clean; 34 new connector tests green, including that the German and New Zealand speed zones are treated identically; the full connector module runs 232 tests with the same single pre-existing failure and 16 pre-existing settings-not-configured errors as the base commit (confirmed against a clean worktree at HEAD); 102 pure measures tests still green; map.html compiles as a Django template and its JavaScript passes node --check. manage.py test and docker compose could not be run in this container — it has Django but no GDAL, so every GeoDjango import fails. CI runs both. Confirming that the crossings layer now stores a non-zero record count needs a live Overpass call and is the first verification step for review. Note for follow-up, not fixed here: `transit_stops` and `trees` use the same `out tags` query and may be silently empty for the same reason. That is a wider blast radius than this change, and deserves its own issue. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CKiSPbDwCocoqdk3zoty9w
richardkfm
marked this pull request as ready for review
September 18, 2026 06:48
richardkfm
pushed a commit
that referenced
this pull request
Sep 18, 2026
Slice 3b of three. The data repairs landed in #93; this turns them into a reading. A new pure module, measures/walkability.py, rates each street comfortable / usable / tight / hostile / unknown from ten factors split across a safety and a comfort component, and exposes it at /api/v1/workspaces/<slug>/walkability/. The whole module is built around one rule: silence is never a middling score. A factor the data cannot speak to returns None, is dropped from the weighted mean rather than substituted with 0.5, and is named in the street's `unknowns` list. Below min_coverage no class is given at all. That is deliberately conservative — a workspace with no footways layer reports most streets as unknown whatever their traffic, because comfort is 40 % of the picture — and the collection's note says so, since the gap is the actionable finding. The footway join is guided proximity: a street's own `sidewalk` record by osm_id first, then, only where that record says `separate` or there is no record at all, the nearest standalone footway within 25 m. A street surveyed as having no pavement is never rescued by a line that happens to run nearby; a street matched by nearness is flagged and capped at medium confidence. Two over-claims found and fixed while re-reading the diff: a separately-mapped footway is one line beside the street, not two, so it now scores as one side and contributes its width once rather than twice; and the osm_id index holds only street-tag records, since a standalone way's id is not a street's. `_SegmentGrid`, `_seg_dist2` and `_iter_linestrings` move from measures/accident_density.py into measures/geo.py, with the old private names left behind as aliases exactly as `_make_projector` already was. No caller moves, and area_engine's import is untouched. Implicit speed zones stay unresolved unless a workspace supplies its own numbers: what `urban` means is a question of national law, and a table of country defaults in core code is the coupling principle 1 forbids. A test asserts DE, NZ, GB and FR zones all behave identically out of the box. The methodology page gains the walkability weights and thresholds, and — while there — the parked-car parameters, which slice 2 shipped overridable but never printed anywhere. No model change, so no migration. Walkability is a derived endpoint like /parked-cars/, not a synced layer kind. Verified: ruff check backend/ clean; 199 pure-module tests green, of which 97 are new; connectors.tests unchanged at its pre-existing 1 failure + 16 "settings are not configured" errors; methodology.html compiles as a Django template; accident_density smoke-tested through the promoted helpers. `manage.py test` and `docker compose up` could not be run — this container has Django but no GDAL, so every GeoDjango import fails. CI runs both. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CKiSPbDwCocoqdk3zoty9w
This was referenced Sep 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Groundwork for the walkability view (slice 3a of three). While planning the walk score, three of its four inputs turned out to be broken or missing — and each one would have made the score lie rather than merely fall short. They are repaired here, on their own, before anything is built on top of them.
Pedestrian crossings synced nothing. The Overpass query ended
out tags, whose verbosity prints ids and tags without node coordinates, so every crossing was discarded by the element converter before it could be stored. The layer reported success and held an empty set. It now asks for geometry, and a new normaliser gives each crossing a stable reading of whether it is signalised, marked, dropped-kerb and tactile — three-state throughout, so a crossing surveyed as unmarked stays distinct from one nobody has looked at.Speed limits were unreadable outside the countries that tag a bare number in km/h.
maxspeedhad no normaliser at all, so20 mphandDE:urbanreached every consumer as raw strings and drew as unknown. They are now parsed once into a comparable figure. Two cases deliberately carry no number: an unrestricted road, because 0 km/h would rank an autobahn as the calmest street in town, and a national zone, because what urban means is a question of local law — the zone is preserved and the workspace supplies the value, rather than a table of one country's defaults being baked into core code.A street whose pavement is mapped separately was recorded as having none.
sidewalk=separatesays the footway exists and is drawn as its own line; it was sitting in the set of values meaning "surveyed, nothing here". Left alone, the walking score would have condemned exactly the cities that map pedestrian space best. It now reads as a pointer —footway_present: nullwithsides: "separate"— and outranks anoon the opposite side, because one side mapped separately still means there is a pavement to find.Also: kissing gates and stiles now report
affects: "walking", a valuedocs/AREA_TARGETS.mdhas described since it was written but which nothing ever emitted; and the map's speed colouring reads the new numeric field, keeping the old string match behind acoalesceguard so workspaces synced before this change render exactly as they do today.No model changes, so no migration.
Type of change
Checklist (CLAUDE.md governance)
CHANGELOG.mdhas a new entry under[Unreleased]README.mdis updated if user-facing behavior changedVERSIONis bumped if warranted (see versioning policy in CLAUDE.md) — 0.51.0 → 0.52.0gettext/{% trans %}— no new user-facing strings in this changedocker compose up --buildruns cleanly and core flows workpython manage.py testpassesThe last two are unticked because they could not be run. This container has Django but no GDAL, so every GeoDjango import fails and neither
manage.py testnordocker compose upcan start. CI runs both. Rather than tick a box on a command I did not execute, they are left open.Testing notes
34 new connector tests, all green, plus the existing suite unchanged:
MaxspeedParserTests— km/h, mph (30 mphand30mph),walk,none, four national zones, nonsense, absence. One test assertsDE:urbanandNZ:urbanparse identically, so that if either ever resolves to a number this feature has acquired a home country and the test says so.StreetsWithSpeedNormalizerTests— the comparable number, the mph city, the preserved zone, and that the raw value always survives whatever we failed to parse.PedestrianCrossingTemplateTests— the template asks for geometry, a crossing node survives the element converter, and a crossing without coordinates is dropped. The last one records the failure mode so a futureout tagscannot creep back in unnoticed.PedestrianCrossingNormalizerTests— signalised, marked, unmarked, level crossing, unknown; tactile paving three-state; an unrecognised kerb value is not invented;crossing:markings=surfaceis a marking (a change of paving, not the absence of paint); signals outrank missing paint.ObstacleAffectsTests— step-over barriers affect walking, ordinary barriers still affect both, tram tracks are still a cycling problem, andaffectsnever leaves its documented domain.FootwayNormalizerTests— thesidewalk=separatetest that asserted the old behaviour is replaced by four: separate is a pointer not an absence; separate stays distinct from silence; separate on one side outranksnoon the other; a surveyed side still wins over separate.Verification run:
ruff check backend/— clean.python3 -m unittest connectors.tests— 232 tests, with the same 1 pre-existing failure and 16 pre-existing "settings are not configured" errors as the base commit. Confirmed by running the identical command in a cleangit worktreeat28e06d4: 198 tests, identical failure counts. None of them touch changed code.python3 -m unittest measures.test_geo measures.test_street_space measures.test_parking_estimate— 102 tests green.map.htmlcompiles as a Django template, and its extracted JavaScript passesnode --check.Not verified, and it is the first thing worth checking in review: that the crossings layer now stores a non-zero record count. That needs a live Overpass call, and outbound access to
overpass-api.deis blocked from this container. Theout tagsdiagnosis is from the Overpass specification and from_osm_element_to_featurereturningNonefor a coordinate-less node —out geom tagsis the correct form for a node query either way, so the fix is safe even if the diagnosis is wrong.Suggested manual check in
docker compose up --build:pedestrian_crossingsin a demo workspace; confirm a non-zero record count and that/api/v1/workspaces/<slug>/features/pedestrian_crossings/returns Point geometries.streets_with_speed; confirmmaxspeed_kmhis populated,maxspeed_sourceis honest, and at least one feature keeps itsmaxspeed_zone.coalescefallback).sidewalk=separatestreet readsfootway_present: null,sides: "separate".Related issues
Follow-up worth filing separately, not fixed here:
transit_stopsandtreesuse the sameout tagsquery and may be silently empty for the same reason. That is a wider blast radius than this change and deserves its own issue rather than a quietly widened pull request.Next in this series: 3b adds
measures/walkability.pyand the/walkability/endpoint; 3c draws the coloured line and the parking-vs-walking story view.🤖 Generated with Claude Code
https://claude.ai/code/session_01CKiSPbDwCocoqdk3zoty9w
Generated by Claude Code