About OLL

Optimal London Living is an informational tool for comparing London locations. It presents factual source data and transparent preference matches; it does not produce a single liveability score or choose a home for you.

Sources differ in date, resolution and coverage. “Data as of” dates below are fetch dates unless a source notes a separate model or measurement period.

Privacy

OLL has no accounts, analytics or tracking cookies. Precise homes, saved areas, the workplace and report location are stored in this browser tab’s session storage. Reloading the same tab retains them; a fresh tab or session starts without them. Harmless interface preferences, such as whether the welcome guide has been seen, may use local storage.

The OLL server receives the locations and text necessary to calculate reports, local search and commutes. Geoapify receives rounded coordinates needed for route or isochrone calculations. OpenFreeMap receives tile requests and, like any network service, can receive an IP address and the approximate map viewport. Production access logs record method, path without query, status, duration and a client address used for abuse controls, and are retained for approximately 14 days.

Ordinary navigation links do not contain precise locations. An explicit Share link does contain the exact locations and names selected for sharing, and anyone with that link can view them.

Data Sources

The register below is generated from the same city manifest checked at application startup and by the developer data audit.

DatasetSource and useLicenceData as of
aircraft_noise Department for Environment, Food and Rural Affairs (Defra), Airport Noise - All Metrics - England Round 4 (2022 modelled data), https://environment.data.gov.uk/dataset/dac9cba4-abe7-43bd-b8e9-8a83da52edd8; Heathrow and London City LAeq,16h WCS coverages.
Limitations and method

Official Round 4 strategic airport-noise rasters, model year 2022, native 10 m grid in EPSG:27700. Only Heathrow and London City intersect Greater London. Cumulative 51/54/57/60/63 dB LAeq,16h contours are clipped to the borough boundary and simplified at 15 m. LAeq,16h represents 07:00-23:00 annual-average exposure. Values are neighbourhood-scale modelled exposure, not property measurements. Source distributions: {"Heathrow": {"maximum_db": 88.1, "minimum_db": 49.0}, "London City": {"maximum_db": 88.3, "minimum_db": 40.0}}. Supersedes the legacy Heathrow-PDF-derived contour, which is excluded from public output because its redistribution rights were unsuitable for public release.

Open Government Licence v3.0 — https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/ 2026-09-30
airport_runways OpenStreetMap contributors, via Overpass API (runway geometry/ref), with operational-use status checked against each airport's own published material.
Limitations and method

Seven reviewed runway references across Heathrow, Gatwick, Luton, Stansted and London City: 6 routine and 1 standby. Each feature is an exactly 10,000m indicator centred on the OSM runway axis to show location/orientation only — not the full runway extent, a flight path, frequency, altitude or wind-dependent direction. The live source must remain exactly 17 runway-tagged ways in five tight airport boxes and contain exactly the seven reviewed ICAO/ref pairs; any mismatch stops the build for manual review. Annually refreshed.

Open Database License (ODbL) v1.0 — https://www.openstreetmap.org/copyright; operational status is factual source attribution via the per-feature source URLs. 2026-09-02
airports OpenStreetMap contributors, via Overpass API: facility-level aeroway=aerodrome or aeroway=heliport in Greater London, plus Gatwick/Luton/Stansted aerodromes matched by ICAO code in a dedicated wider bbox.
Limitations and method

16 aerodromes and 1 facility-level heliport. Heliports require the literal aeroway=heliport tag: all aeroway=helipad features are excluded, and names/operators never promote another feature type. Every aerodrome in Greater London remains included regardless of size, plus EGKK/EGGW/EGSS outside the boundary. Point locations are OSM centroids (out center;). Annually refreshed.

Open Database License (ODbL) v1.0 — https://www.openstreetmap.org/copyright 2026-09-02
aldi OpenStreetMap contributors, via Overpass API (shop=supermarket, brand=Aldi)
Limitations and method

Classified from a single shop=supermarket query (Overpass out center;) — see scripts/fetch_supermarkets.py's module docstring for the matching heuristic (brand tag, falling back to brand:wikidata only when brand is absent; never guessed from the freeform name tag). Of 1371 shop=supermarket features found in the bbox: 170 carry a recognized brand that isn't one of these 9 tracked chains (Amazon Fresh, Best Foods, Budgens, Costcutter, Eataly, H Mart, Jumbo, Londis, Longdan, Lycamobile, Market Basket, Mega Saver, Nisa, Nisa Extra, Nisa Local, Planet Organic, Spar, The Food Warehouse, The Source Bulk Foods, Waitrose, Waitrose & Partners, Whole Foods Market, Wholefoods Market), and 550 have no usable brand tag at all (neither brand nor brand:wikidata) — logged during fetch, not silently dropped and not assigned a brand by guessing. Centroid only (out center;), Greater London bbox. Waitrose is fetched separately by scripts/fetch_waitrose.py (matched by name, predates this script).

Open Database License (ODbL) v1.0 — https://www.openstreetmap.org/copyright 2026-08-24
asda OpenStreetMap contributors, via Overpass API (shop=supermarket, brand=Asda)
Limitations and method

Classified from a single shop=supermarket query (Overpass out center;) — see scripts/fetch_supermarkets.py's module docstring for the matching heuristic (brand tag, falling back to brand:wikidata only when brand is absent; never guessed from the freeform name tag). Of 1371 shop=supermarket features found in the bbox: 170 carry a recognized brand that isn't one of these 9 tracked chains (Amazon Fresh, Best Foods, Budgens, Costcutter, Eataly, H Mart, Jumbo, Londis, Longdan, Lycamobile, Market Basket, Mega Saver, Nisa, Nisa Extra, Nisa Local, Planet Organic, Spar, The Food Warehouse, The Source Bulk Foods, Waitrose, Waitrose & Partners, Whole Foods Market, Wholefoods Market), and 550 have no usable brand tag at all (neither brand nor brand:wikidata) — logged during fetch, not silently dropped and not assigned a brand by guessing. Centroid only (out center;), Greater London bbox. Waitrose is fetched separately by scripts/fetch_waitrose.py (matched by name, predates this script).

Open Database License (ODbL) v1.0 — https://www.openstreetmap.org/copyright 2026-08-24
borough_boundaries ONS Open Geography Portal, 'Local Authority Districts (May 2025) Boundaries UK' (https://services1.arcgis.com/ESMARspQHYMw9BZ9/arcgis/rest/services/LAD_MAY_2025_UK_BFC_V2/FeatureServer/0/query), filtered to London boroughs (LAD25CD LIKE 'E09%'); council tax figures from gov.uk/MHCLG 'Council Tax levels set by local authorities in England 2026 to 2027', Table 9 (Area council tax for a dwelling occupied by 2 adults by band) — https://assets.publishing.service.gov.uk/media/69de1fa63e81003ae0422508/Tables_1-9_2026-27.ods — dataset page: https://www.gov.uk/government/statistics/council-tax-levels-set-by-local-authorities-in-england-2026-to-2027
Limitations and method

33 London boroughs (32 boroughs + City of London), sourced from ONS rather than TfL's own borough boundary layer (which also exists but is GLA data from June 2017 with older, non-GSS codes) — ONS is more current and its GSS codes (E09000007, etc.) are the exact join key the council tax table below uses. Each feature carries its own Council Tax Band A-H figures (council_tax_band_a .. council_tax_band_h) as properties, shown in a click popup on the map — the one polygon layer in this project with per-feature click detail, unlike green_spaces/noise/flood_risk/crime which stay click-free by design. FIRST spreadsheet-shaped source this project has used (everything else is GeoJSON/CSV/a queryable API) — parsed against the REAL downloaded file's actual sheet name (Table_9) and column layout (header row at index 2, 'ONS Code'/'Band A'..'Band H' columns), verified directly rather than assumed from a prior year's structure; the parser raises loudly rather than silently misreading if those columns ever move. Joined to boundaries by ONS/GSS code, not authority name, since the two sources' name strings aren't guaranteed to match character-for-character. Simplified to ~10m tolerance in EPSG:27700 (this project's usual range — 33 large, simple polygons, nothing like road/rail noise's sprawling-network case that needed a coarser figure). Council tax figures are current for the 2026-27 tax year (published 25 March 2026) and will need a fresh Table 9 URL next April when the following year's release replaces it — review cycle is 365 days, same as this project's other static boundary layers, since the boundary geometry itself barely changes even though the tax figures are annual. No radius buffer options — a reference/boundary layer, not a proximity POI layer. Reference/awareness layer only — display, not a Phase 2 scoring input.

Open Government Licence v3.0 — https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/ 2026-08-26
cafes OpenStreetMap contributors, via Overpass API (amenity=cafe)
Limitations and method

Greater London bbox; nodes/ways/relations represented by one centroid using Overpass out center;. Name/brand/addr:*, normalized cuisine/opening_hours/website and OSM provenance are stored. Clustered at wider zooms. Optional detail coverage in this snapshot: website 1822/7180; OSM-listed opening hours 1892/7180; cuisine 2745/7180; human-readable brand 1203/7180. Missing optional fields are omitted, never shown as unavailable.

Open Database License (ODbL) v1.0 — https://www.openstreetmap.org/copyright 2026-09-03
coop OpenStreetMap contributors, via Overpass API (shop=supermarket, brand=Co-op)
Limitations and method

Classified from a single shop=supermarket query (Overpass out center;) — see scripts/fetch_supermarkets.py's module docstring for the matching heuristic (brand tag, falling back to brand:wikidata only when brand is absent; never guessed from the freeform name tag). Of 1371 shop=supermarket features found in the bbox: 170 carry a recognized brand that isn't one of these 9 tracked chains (Amazon Fresh, Best Foods, Budgens, Costcutter, Eataly, H Mart, Jumbo, Londis, Longdan, Lycamobile, Market Basket, Mega Saver, Nisa, Nisa Extra, Nisa Local, Planet Organic, Spar, The Food Warehouse, The Source Bulk Foods, Waitrose, Waitrose & Partners, Whole Foods Market, Wholefoods Market), and 550 have no usable brand tag at all (neither brand nor brand:wikidata) — logged during fetch, not silently dropped and not assigned a brand by guessing. Centroid only (out center;), Greater London bbox. Waitrose is fetched separately by scripts/fetch_waitrose.py (matched by name, predates this script).

Open Database License (ODbL) v1.0 — https://www.openstreetmap.org/copyright 2026-08-24
crime data.police.uk bulk crime data (Metropolitan Police Service + City of London Police, trailing 12 months: 2025-09 to 2026-08), custom-download tool (https://data.police.uk/data/); Output Area (Dec 2021) boundaries via ONS Open Geography Portal's generalised (BGC) FeatureServer (https://services1.arcgis.com/ESMARspQHYMw9BZ9/arcgis/rest/services/Output_Areas_2021_EW_BGC_V2/FeatureServer/0/query); Census 2021 TS001 usual resident population via Nomis bulk download (https://www.nomisweb.co.uk/output/census/2021/census2021-ts001.zip)
Limitations and method

H3 hex grid, resolution 8 (~0.74 sq km / ~461m edge), covering config.GREATER_LONDON_BBOX. Each crime record binned directly into its containing hex by lat/lon (point-in-hex), not point-in-area-then-inherit. 7 category buckets + a combined 'all' total — see CRIME_CATEGORY_BUCKETS in the fetch script for the exact raw-category-to-bucket mapping, including the judgement call behind 'Robbery & Theft' (combines Robbery, Theft from the person, Other theft, Bicycle theft, and Shoplifting into one bucket, since the brief named this bucket without specifying which raw categories belong in it). TRAILING 12-MONTH WINDOW, computed dynamically each run from data.police.uk's own archive listing (not a hardcoded date range) — this is why review_every_days is 30 days, notably shorter than this project's other reference layers (365 days): re-running this script is how the window actually moves forward, not just a staleness check. AREA NORMALISATION (Phase 24 — raw counts, not per capita): every hex carries a raw reported-incident count and a count PER KM^2 (count / h3.cell_area), per category and combined — rendered by per km^2. This layer previously divided the count by apportioned resident population (a 'per 1,000 residents' rate); that denominator broke down where the phase's brief said it would — commercial / nightlife hexes (Oxford Street, Soho, Camden Market) have a few thousand residents but tens of thousands of reported incidents, inflating the rate from a small denominator, while parks and reservoirs have ~0 residents. Per km^2 is area-normalised, so it stays defined everywhere AND stays comparable across the two resolutions (crime res-8 and crime_coarse res-6 share one colour scale — a res-6 hex is ~49x a res-8 hex, so raw counts would not survive the swap; per km^2 does). It does NOT correct for footfall/reporting bias — a busy commercial district still ranks high because a lot of crime is genuinely reported there; the legend and info-icon copy say so plainly. Population is STILL apportioned into each hex by LAND-AREA OVERLAP with the Census 2021 OUTPUT AREAS it intersects (an areal-interpolation assumption: population treated as uniformly spread within each source OA — a standard small-area-estimate simplification), via a shapely STRtree spatial index rather than a full scan, and written per hex, because build_discovery_grid.py weights its other criteria by it — it just no longer divides the crime count. UPGRADED FROM LSOA (~1,000-3,000 residents each) TO OA (~100-625) for materially less-lossy apportionment at this grid's ~461m hex size — same source family (ONS Open Geography Portal / Nomis), same apportionment logic, just a finer source geography; London's ~25,000 OAs (vs ~5,000 LSOAs) make this step slower to run but change nothing about how it works. Hexes with fewer than 1.0 apportioned residents (runways, reservoirs, open water — real geometry inside the bbox that nobody lives near) are dropped entirely; areal interpolation still gives genuine park hexes a few hundred apportioned residents, so parks are NOT dropped. Reference/awareness layer only — display, not a Phase 2 scoring input. Every crime location published by police forces is deliberately anonymised, snapped to the nearest of roughly 750,000 fixed map points (never an exact address) — a real precision limit inherited from the source, not introduced by this project's own aggregation. Some records have no location published at all (logged/skipped, not silently dropped — see the fetch script's own run output for the real count). Reflects crime reported and recorded by police, not actual crime levels, which can differ meaningfully by area and crime type.

Open Government Licence v3.0 — https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/ 2026-09-30
crime_coarse Derived from crime.geojson (this same script, same run) by aggregating resolution-8 hexes up to resolution 6 via H3's own cell_to_parent — not a second independent fetch or binning pass over the raw crime records.
Limitations and method

H3 hex grid, resolution 6 (~36 sq km / ~3.2km edge) — a zoomed-out companion to crime.geojson (resolution 8), swapped in client-side below a zoom threshold (see app/static/js/layers.js's CRIME_COARSE_ZOOM_THRESHOLD) so a borough-wide or phone-sized view doesn't render all ~3,400 fine hexes at once (UX pass item 10, zoom-gating audit — see CLAUDE.md/CHANGELOG.md). Built by aggregating the fine grid's own already-computed population/counts UP via H3's cell_to_parent, then recomputing each per-km^2 value as SUMMED count / the coarse parent's OWN area (not averaging the fine hexes' own per-km^2 values). Same category buckets, same population-floor drop rule (< 1.0 apportioned residents) as the fine grid. The colour SCALE (95th-percentile cap per category) is deliberately shared with the fine grid, not recomputed separately for this resolution — see layers.js — so the same per-km^2 value reads as the same colour regardless of which resolution is currently shown. This cross-resolution stability is exactly why Phase 24 chose per km^2 over pure raw counts (a res-6 hex is ~49x a res-8 hex).

Open Government Licence v3.0 — https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/ 2026-09-30
cycle_injuries Department for Transport STATS19 Road Safety Open Data, latest five final years (2021-2025) collision and casualty files
Limitations and method

One pin per reported serious (5,098) or fatal (39) pedal-cycle casualty in Greater London, 2021-2025. Slight-injury cyclist casualties (58,238) are excluded. A further 5 serious/fatal cyclist casualties had no usable collision coordinate and 14,361 were outside Greater London. Each pin sits at its collision's recorded coordinate. STATS19 covers personal-injury collisions reported to police, not all cycling incidents, and injury-severity reporting methods changed across police forces over this period, so recent-year serious counts are not perfectly comparable to earlier years. There is no cycling-exposure denominator, so this is a count of reported harm, not a safety rate. Vehicle data is not used. Direct files: https://data.dft.gov.uk/road-accidents-safety-data/dft-road-casualty-statistics-collision-last-5-years.csv and https://data.dft.gov.uk/road-accidents-safety-data/dft-road-casualty-statistics-casualty-last-5-years.csv.

Open Government Licence v3.0 — https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/ 2026-09-03
cycle_lanes TfL Cycle Routes open dataset (https://cycling.data.tfl.gov.uk/CycleRoutes/CycleRoutes.json)
Limitations and method

166 of 170 routes from TfL's official Cycle Routes dataset (supersedes the earlier Cycling Infrastructure Database (CID) build, which classified 24,976 raw lane SEGMENTS by inferred protection level rather than showing named ROUTES — CID answers 'what cycling infrastructure exists', this dataset answers 'where are London's recognised current cycle routes', which is what this layer is actually for; see fetch_cycle_lanes.py's own module docstring and docs/decisions/cycle-lanes.md). Includes OPEN Cycleways and legacy Cycle Superhighways only ({'Cycleways': 162, 'Cycle Superhighways': 4}); EXCLUDES Quietways and any non-Open (in-progress/planned/future) route — TfL has migrated almost the entire historical Superhighway/Quietway network into numbered Cycleways since 2019, so only the routes TfL's own current record still carries under those legacy Programme values are shown. LineString/MultiLineString geometry. No radius buffers (NO_RADIUS_BUFFER_LAYERS) — a route isn't a point you're near or far from.

Open Government Licence v3.0 (TfL Open Data) — https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/. Requires "Powered by TfL Open Data" attribution — see app/static/js/map-init.js. 2026-09-20
deprivation_imd MHCLG English Indices of Deprivation 2025, File 1 (IMD25 ranks and deciles) — https://assets.publishing.service.gov.uk/media/691dece32c6b98ecdbc500d5/File_1_IoD2025_Index_of_Multiple_Deprivation.xlsx. Enriches the existing London LSOA polygons built by fetch_population_density.py (2021 LSOA boundaries, same vintage IMD25 itself uses) rather than duplicating them — see docs/decisions/deprivation-imd.md.
Limitations and method

imd_rank/imd_decile fields added to population_density.geojson's existing features (shared geometry, not a separate file). Decile 1 = most deprived 10% of LSOAs in England (national ranking, not re-ranked within London). Re-running fetch_population_density.py overwrites this file from scratch — rerun this script afterward to restore these fields.

Open Government Licence v3.0 — https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/ 2026-09-20
discovery_grid Derived (no network) from this project's own committed processed layers — crime.geojson/crime_coarse.geojson (H3 grid + apportioned Census population + reported crimes per km²), schools.geojson (Good/Outstanding state schools), green_spaces.geojson, road_noise/rail_noise/aircraft_noise.geojson, borough_boundaries.geojson, underground/overground_rail.geojson — plus postcode_index.sqlite and rental_prices.sqlite (address-weighted district rent blend with official dominant-overlap borough fallback) and recently_sold.sqlite (per-hex median sale price).
Limitations and method

Guided-discovery matching grid (checkpoint 4; res-8 primary + res-6 fallback added later — see CLAUDE.md's Architecture-decisions section). NOT a rendered sidebar layer and not in app/services/layers.py's LAYERS — served only by /api/discovery/matches. Rebuild whenever any source layer is refreshed — a pure derivation, no network. The PRIMARY guided-discovery matching grid — one feature per H3 res-8 hex (2598 of them, reusing crime.geojson's own res-8 hexes whose res-6 parent survives the London-substantial filter). Each graduated fraction is measured by sampling the hex at its centroid + H3 boundary vertices (~7 points); safety_frac is 1.0/0.0 against the hex's OWN reported crimes per km². rent_blend_* prefers postcode-district data, then uses the official dominant-overlap borough median only where the district value is missing; rent_resolution_* makes that provenance explicit. buy_median_inherited=true marks a hex whose sale-price figure came from its res-6 parent for want of enough nearby sales — the area card flags both (CLAUDE.md's resolution-honesty rule). Safety cutoffs are derived from crime.geojson, which Phase 24 transformed in place from a per-1,000-residents rate to reported crimes per km² (still LSOA-apportioned); they will shift on the next OA-based crime refresh. Rent coverage (postcode-district / borough fallback / unavailable): room 1126/1273/199, studio 1264/1137/197, 1-bed 2174/227/197, 2-bed 2222/179/197, 3-bed 2211/188/199, 4+-bed 1979/420/199. 197 hex(es) have no positive London-borough overlap and remain unavailable; no nearest-borough imputation.

Derived work — inherits the licences of its source layers (OGL v3.0 / ODbL / OS OpenData), see their own manifest entries. 2026-09-30
discovery_grid_coarse Derived (no network) from this project's own committed processed layers — crime.geojson/crime_coarse.geojson (H3 grid + apportioned Census population + reported crimes per km²), schools.geojson (Good/Outstanding state schools), green_spaces.geojson, road_noise/rail_noise/aircraft_noise.geojson, borough_boundaries.geojson, underground/overground_rail.geojson — plus postcode_index.sqlite and rental_prices.sqlite (address-weighted district rent blend with official dominant-overlap borough fallback) and recently_sold.sqlite (per-hex median sale price).
Limitations and method

Guided-discovery matching grid (checkpoint 4; res-8 primary + res-6 fallback added later — see CLAUDE.md's Architecture-decisions section). NOT a rendered sidebar layer and not in app/services/layers.py's LAYERS — served only by /api/discovery/matches. Rebuild whenever any source layer is refreshed — a pure derivation, no network. The zoomed-out fallback for discovery_grid — one feature per H3 res-6 hex (56 of them, the same set as crime_coarse.geojson). Each fraction is the POPULATION-WEIGHTED share of the hex's populated res-8 children that satisfy the criterion. Rendered client-side below discovery-matches.js's zoom threshold; discovery_grid (res-8) is what shows when zoomed in. Rent coverage (postcode-district / borough fallback / unavailable): room 47/9/0, studio 46/10/0, 1-bed 56/0/0, 2-bed 56/0/0, 3-bed 56/0/0, 4+-bed 55/1/0. 0 hex(es) have no positive London-borough overlap and remain unavailable; no nearest-borough imputation.

Derived work — inherits the licences of its source layers (OGL v3.0 / ODbL / OS OpenData), see their own manifest entries. 2026-09-30
flood_risk Environment Agency, 'Flood Map for Planning' unified Flood Zones dataset (supersedes the retired separate Flood Zone 2 / Flood Zone 3 datasets), queried via its live WFS endpoint (NOT the dataset page's own bulk download links, which are dead — see this script's module docstring): https://environment.data.gov.uk/spatialdata/flood-map-for-planning-flood-zones/wfs (typeNames=dataset-04532375-a198-476e-985e-0579a0a11b47:Flood_Zones_2_3_Rivers_and_Sea) — dataset page: https://environment.data.gov.uk/dataset/04532375-a198-476e-985e-0579a0a11b47
Limitations and method

DEAD LINK WARNING: the dataset page own bulk download links (Flood_Map_for_Planning_Flood_Zones.geojson.zip/.gpkg.zip/.gdb.zip, hosted at api.agrimetrics.co.uk) do not resolve at all — confirmed via two independent DNS lookups, not a transient outage. This script uses the same dataset page live WFS endpoint instead, bbox-scoped to Greater London and paginated — the same server-side-query shape as every Overpass-based POI layer in this project, not an exotic workaround. Revision date on the dataset page: May 2026 (current, unlike the Noise group stale 2012 source). Supersedes the old separate Flood Zone 2 / Flood Zone 3 datasets (retired, links removed after April 2025) — the unified Flood Zones dataset WFS layer already excludes Zone 1 (the residual low-risk category), so no zone-1 filtering was needed. ZONE 3 IS NOT NESTED INSIDE ZONE 2 — CHECKED, NOT ASSUMED, AND THIS IS PERMANENT, NOT A BUG: a geometric nesting check on the real unioned data found 99.52% of Zone 3 area falls OUTSIDE Zone 2 — essentially total non-overlap, not an imperfect edge case. Root cause: the EA own field definitions describe EXCLUSIVE, adjacent probability bands (Zone 2: 0.1%-1% annual probability, Zone 3: 1% or greater) — the same structure as the noise layers raw NoiseClass bands, not a cumulative Zone-2-already-includes-Zone-3 relationship the way medium-and-high-risk-area is loosely described in planning documents. DECISION: rendered as two SEPARATE, independently-coloured, NOT nested polygons rather than forcing this project usual cumulative darker=more severe convention (heathrow_noise/road_noise/rail_noise) onto genuinely disjoint bands, which would misrepresent what the zones mean — building a Zone2-union-Zone3 outer band to force proper nesting was considered and rejected given the cost (each union here already takes 15-30+ minutes) for a visually-consistent-but-inaccurate result. Deliberate, explained difference from every other banded layer in this project, not an inconsistency to fix later — see CLAUDE.md/CHANGELOG.md for the full discovery story. UNION METHOD VERIFIED, NOT ASSUMED to transfer from road/rail noise: tested shapely.coverage_union_all (the noise layers fix) against this real data first — it raised a GEOSException (TopologyException: side location conflict) unioning Zone 3, a hard, unambiguous failure. Zone 2 coverage_union_all call did NOT raise, producing 132,688 output parts — worth noting this alone is not proof of a bad result: plain unary_union on the same input independently produced almost the same count (132,670), so this dataset genuinely has that much real fragmentation before filtering; the Zone 3 exception is the real reason coverage_union_all does not work here. This dataset simply does not form the exact planar partition coverage_union_all needs the way the noise shapefiles raster grid cells did, so this script uses plain unary_union. Hole/part-filter thresholds and simplify tolerance were ALSO verified empirically against this dataset own real union output rather than reused unchanged from road/rail noise tuning — this dataset features are already large, pre-aggregated WFS records (median ~3.5KB each), not tiny raster grid cells: 2000sqm part filter, 2000sqm hole filter. Simplify tolerance needed a correction after the fact, not just a one-shot guess: this project usual ~8-10m range was tried first and technically worked (no errors), but produced a 14.8MB file, well above this project size norms (green_spaces ~5MB, road_noise 5.1MB) — checking output size, not just did-it-run, caught this. Re-simplifying the ALREADY-processed geometry (cheap — no need to redo the expensive union) at 20m — still far short of road/rail noise deliberately coarse 30m, since this dataset already-large features do not need that extreme a departure — cut vertex count ~47-48% for a final ~7.7MB file. No radius buffer options — a reference/boundary layer, not a proximity POI layer (same reasoning as the Noise group). Reference/awareness layer only — display, not a Phase 2 scoring input.

Open Government Licence v3.0 — https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/ 2026-08-26
football_pitches OpenStreetMap contributors via Overpass API (sport=soccer on a physical leisure facility), fetched by Greater London bbox then clipped to the authoritative borough boundary
Limitations and method

sport=soccer; leisure in pitch, practice_pitch, sports_centre, sports_hall, stadium. Nodes/ways/relations become representative points via out center; 392 bbox result(s) outside Greater London removed; deduplicated by OSM type/id. access=private and access=no excluded; missing access retained as unknown (2055/2123), never claimed public. Name, operator, access, fee, lit and surface retained when tagged. Clustered below zoom 13 and individual at zoom 13+.

Open Database License (ODbL) v1.0 — https://www.openstreetmap.org/copyright 2026-09-30
geocode_index Ordnance Survey OS Open Names (GB, CSV bulk download)
Limitations and method

Bulk CSV download (no API key), filtered to a buffered Greater London bbox, reprojected BNG (EPSG:27700) -> WGS84 with pyproj (network transforms disabled — ballpark/Helmert accuracy, a few tens of metres, which is fine given this resolves to postcode/street level, not exact addresses, per the accepted trade-off in the brief.

OS OpenData Licence (OGL v3.0-compatible) — "Contains OS data (c) Crown copyright and database rights 2026." https://os.uk/opendata/licence 2026-08-21
gp_surgeries CQC 'Care directory with ratings' (https://www.cqc.org.uk/system/files/2026-08/04_August_2026_Latest_ratings.ods), filtered to Location Primary Inspection Category 'GP Practices', Domain 'Overall'; enriched with NHS ODS epraccur GP practice list (https://www.odsdatasearchandexport.nhs.uk/api/getReport?report=epraccur) for phone numbers
Limitations and method

1088 London GP surgeries, 0 of them 'Not Rated' (shown plainly with an explanatory note, not hidden — see rating_note() in the fetch script and CLAUDE.md). CQC's own 'Location Primary Inspection Category' drives which rows count as a real GP surgery, NOT epraccur's documented 'Prescribing Setting' field — that field's real value in the downloaded file didn't match its own documentation (a commissioner-code-shaped string, not the small integer NHS's reference data catalogue describes), so this project relies on CQC's own service-type classification instead. Geocoded via postcode_index.sqlite (scripts/fetch_code_point_open.py's unit-postcode index, reused from fetch_recently_sold.py rather than a third geocoding path).

Open Government Licence v3.0 — https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/ 2026-08-27
green_spaces OpenStreetMap contributors, via Overpass API (leisure=park/nature_reserve, landuse=recreation_ground)
Limitations and method

Full polygon/multipolygon outlines via `out geom;` (unlike every other POI layer, which is centroid-only) — multipolygon relations (e.g. a park with an internal lake) correctly assembled into holes via osm2geojson. OSM frequently splits one real place into several features (a road cutting a park in two, a separately-tagged sub-area) — every fetch dissolves those back together (5871 -> 5692 features, 179 merged away): same-name features that overlap/touch/come within 1.5m of each other merge into one (different names never merge into each other even if adjacent — most same-name duplicates in this dataset turned out to be genuinely different places km apart, e.g. 8 separate "King George's Field"s, correctly left alone); unnamed features merge by adjacency alone, independently of any named neighbour (a real, deliberate limitation — an unnamed sub-area touching a named park is not folded into it). Every merged feature's properties record source_feature_count for auditability. Simplified to ~8m tolerance with shapely (reprojected to EPSG:27700 to simplify in metres, then back to WGS84) *before* dissolving, not after — simplification shifts vertices by up to that tolerance, which can bring two features just close enough afterward to violate the dissolve's own touch tolerance; running dissolve last means it has final say on the geometry that actually ships. After dissolving, an area filter drops any UNNAMED feature smaller than 2000sqm (0.2 ha) — small unnamed slivers left over even after merging fragments are more likely mapping artefacts than real, known places (5692 -> 4489: 1203 dropped). NAMED features are kept regardless of size (431 small-but-named features kept this run) — a name is treated as evidence of a real, known place, not itself subject to the threshold. Runs after dissolve, not before, so area reflects each feature's whole merged shape. Threshold is config.GREEN_SPACE_MIN_AREA_SQM, a starting judgement call (0.2 hectares), not a researched figure — tune there if it stops feeling right. Greater London bbox. leisure=garden deliberately excluded — matches ~24,600 individually mapped private residential gardens in this bbox, not public green space. Radius circles for this layer are precomputed, unioned border-buffers (green_spaces_buffer_*.geojson, see the matching manifest entries), not per-feature circles. Phase 14: every feature also carries an is_major boolean (area >= 5 ha) that the frontend's zoom-tier gate reads to draw only major parks below zoom 12 — the full dataset is unchanged and still ships. green_spaces_major_buffer_*.geojson is the matching major-subset radius buffer.

Open Database License (ODbL) v1.0 — https://www.openstreetmap.org/copyright 2026-08-24
gyms OpenStreetMap contributors, via Overpass API (leisure=fitness_centre)
Limitations and method

Greater London bbox; nodes/ways/relations represented by one centroid using Overpass out center;. Name/brand/addr:*, normalized opening_hours/website and OSM provenance are stored. Clustered at wider zooms. Optional detail coverage in this snapshot: website 595/1067; OSM-listed opening hours 225/1067; human-readable brand 349/1067. Missing optional fields are omitted, never shown as unavailable. Gym size was not published: 0/1067 source features had a capacity/area/floor-area/length/width value, while building:levels and level describe a building or floor location rather than gym size.

Open Database License (ODbL) v1.0 — https://www.openstreetmap.org/copyright 2026-09-03
hospitals OpenStreetMap contributors, via Overpass API (amenity=hospital)
Limitations and method

Nodes/ways/relations tagged amenity=hospital, centroid only (out center;), Greater London bbox. Each feature also carries a computed is_major boolean (OSM emergency=yes — 33 of 226 hospitals) for the major/minor marker styling — see this script's own module docstring for why that's a narrow, scoped exception to this project's usual name/brand/addr:*-only POI tag rule.

Open Database License (ODbL) v1.0 — https://www.openstreetmap.org/copyright 2026-08-28
iceland OpenStreetMap contributors, via Overpass API (shop=supermarket, brand=Iceland)
Limitations and method

Classified from a single shop=supermarket query (Overpass out center;) — see scripts/fetch_supermarkets.py's module docstring for the matching heuristic (brand tag, falling back to brand:wikidata only when brand is absent; never guessed from the freeform name tag). Of 1371 shop=supermarket features found in the bbox: 170 carry a recognized brand that isn't one of these 9 tracked chains (Amazon Fresh, Best Foods, Budgens, Costcutter, Eataly, H Mart, Jumbo, Londis, Longdan, Lycamobile, Market Basket, Mega Saver, Nisa, Nisa Extra, Nisa Local, Planet Organic, Spar, The Food Warehouse, The Source Bulk Foods, Waitrose, Waitrose & Partners, Whole Foods Market, Wholefoods Market), and 550 have no usable brand tag at all (neither brand nor brand:wikidata) — logged during fetch, not silently dropped and not assigned a brand by guessing. Centroid only (out center;), Greater London bbox. Waitrose is fetched separately by scripts/fetch_waitrose.py (matched by name, predates this script).

Open Database License (ODbL) v1.0 — https://www.openstreetmap.org/copyright 2026-08-24
lidl OpenStreetMap contributors, via Overpass API (shop=supermarket, brand=Lidl)
Limitations and method

Classified from a single shop=supermarket query (Overpass out center;) — see scripts/fetch_supermarkets.py's module docstring for the matching heuristic (brand tag, falling back to brand:wikidata only when brand is absent; never guessed from the freeform name tag). Of 1371 shop=supermarket features found in the bbox: 170 carry a recognized brand that isn't one of these 9 tracked chains (Amazon Fresh, Best Foods, Budgens, Costcutter, Eataly, H Mart, Jumbo, Londis, Longdan, Lycamobile, Market Basket, Mega Saver, Nisa, Nisa Extra, Nisa Local, Planet Organic, Spar, The Food Warehouse, The Source Bulk Foods, Waitrose, Waitrose & Partners, Whole Foods Market, Wholefoods Market), and 550 have no usable brand tag at all (neither brand nor brand:wikidata) — logged during fetch, not silently dropped and not assigned a brand by guessing. Centroid only (out center;), Greater London bbox. Waitrose is fetched separately by scripts/fetch_waitrose.py (matched by name, predates this script).

Open Database License (ODbL) v1.0 — https://www.openstreetmap.org/copyright 2026-08-24
lidos OpenStreetMap contributors, via Overpass API (public outdoor swimming pools / lidos)
Limitations and method

Greater London bbox; nodes/ways/relations represented by one centroid using Overpass out center;. Name/brand/addr:*, poi_category, normalized opening_hours/phone/website and OSM provenance are stored. Plain markers. Optional detail coverage in this snapshot: website 8/12; OSM-listed opening hours 5/12; phone 3/12; human-readable brand 2/12. Missing optional fields are omitted, never shown as unavailable. Phase 22 taxonomy: a named lido facility (leisure=sports_centre/swimming_area/water_park, name~lido or water_park=lido) OR a genuinely public outdoor swimming-pool polygon; private/kids/paddling/indoor pools excluded, unnamed polygons kept only with a fee/operator/length>=20m; facility and water records within 200m merged to the richest. Conservative — outdoor pools tagged as neither still fall outside the layer.

Open Database License (ODbL) v1.0 — https://www.openstreetmap.org/copyright 2026-09-03
marks_spencer OpenStreetMap contributors, via Overpass API (shop=supermarket, brand=M&S Food)
Limitations and method

Classified from a single shop=supermarket query (Overpass out center;) — see scripts/fetch_supermarkets.py's module docstring for the matching heuristic (brand tag, falling back to brand:wikidata only when brand is absent; never guessed from the freeform name tag). Of 1371 shop=supermarket features found in the bbox: 170 carry a recognized brand that isn't one of these 9 tracked chains (Amazon Fresh, Best Foods, Budgens, Costcutter, Eataly, H Mart, Jumbo, Londis, Longdan, Lycamobile, Market Basket, Mega Saver, Nisa, Nisa Extra, Nisa Local, Planet Organic, Spar, The Food Warehouse, The Source Bulk Foods, Waitrose, Waitrose & Partners, Whole Foods Market, Wholefoods Market), and 550 have no usable brand tag at all (neither brand nor brand:wikidata) — logged during fetch, not silently dropped and not assigned a brand by guessing. Centroid only (out center;), Greater London bbox. Waitrose is fetched separately by scripts/fetch_waitrose.py (matched by name, predates this script).

Open Database License (ODbL) v1.0 — https://www.openstreetmap.org/copyright 2026-08-24
morrisons OpenStreetMap contributors, via Overpass API (shop=supermarket, brand=Morrisons)
Limitations and method

Classified from a single shop=supermarket query (Overpass out center;) — see scripts/fetch_supermarkets.py's module docstring for the matching heuristic (brand tag, falling back to brand:wikidata only when brand is absent; never guessed from the freeform name tag). Of 1371 shop=supermarket features found in the bbox: 170 carry a recognized brand that isn't one of these 9 tracked chains (Amazon Fresh, Best Foods, Budgens, Costcutter, Eataly, H Mart, Jumbo, Londis, Longdan, Lycamobile, Market Basket, Mega Saver, Nisa, Nisa Extra, Nisa Local, Planet Organic, Spar, The Food Warehouse, The Source Bulk Foods, Waitrose, Waitrose & Partners, Whole Foods Market, Wholefoods Market), and 550 have no usable brand tag at all (neither brand nor brand:wikidata) — logged during fetch, not silently dropped and not assigned a brand by guessing. Centroid only (out center;), Greater London bbox. Waitrose is fetched separately by scripts/fetch_waitrose.py (matched by name, predates this script).

Open Database License (ODbL) v1.0 — https://www.openstreetmap.org/copyright 2026-08-24
overground_lines OpenStreetMap, via Overpass API — route=train relations tagged network=London Overground OR network:metro=London Overground, grouped by their shared ref tag (one line = several direction/branch relations), recursed down to member ways, deduplicated at the way level.
Limitations and method

All 6 current Overground lines (the November 2024 named-lines rebrand), each its own MultiLineString feature (one component per unique OSM way). COLOUR CONFIDENCE FLAGGED: these six hex values are lower-confidence than the Underground set — sourced from one reference (a Wikipedia route-diagram module documented as derived from TfL's own standards) and only cross-checked at the colour-FAMILY level against independent rebrand news coverage, not matched byte-for-byte against a second independent hex source. OSM's own colour/ref:colour tags (found while rewriting this script's geometry source) differ somewhat from these values but weren't adopted — see module docstring. Route geometry from OpenStreetMap's own railway route relations via Overpass (NOT TfL's Route/Sequence API any more — that turned out to be essentially straight lines between consecutive station stops, not real track shape; see this script's own module docstring for the full story), traced once and cached as a static file. No radius buffer — a transit line has no meaningful 'buffer around a route' concept.

Open Database License (ODbL) v1.0 — https://www.openstreetmap.org/copyright 2026-08-28
overground_rail OpenStreetMap contributors, via Overpass API (railway=station/halt, classified as Overground/DLR/Elizabeth line/National Rail — not London Underground)
Limitations and method

Split from a single railway=station/halt query (Overpass out center;) via tags already present in the data — see scripts/fetch_stations.py's module docstring for the exact heuristic. 41 station(s) are genuine interchanges and appear in BOTH this layer and its counterpart (e.g. Bank, Farringdon, Highbury & Islington) — an interchange should count for proximity to each mode it serves, not be forced into one bucket. 11 station(s) excluded from both layers as heritage/miniature railways (usage=tourism), not real transit: 'Willow Lawn', 'Woody Bay', 'Cassiobury Park Station', 'Haste Hill', 'Depot Approach', 'Ealing End', 'Swanley New Barn Miniature Railway', 'Hanworth Halt', "Eddie's Rest", None, 'Barking Park'. Ambiguous case resolved during development: secondary OSM nodes at Liverpool Street and Paddington carry only operator="Transport for London" (no station=subway or network=London Underground tag) — added as a third Underground signal after finding these. Greater London bbox, centroid only (out center;), like every POI layer except green_spaces.

Open Database License (ODbL) v1.0 — https://www.openstreetmap.org/copyright 2026-08-21
places_of_worship OpenStreetMap contributors, via Overpass API (amenity=place_of_worship, religion=christian/muslim/jewish)
Limitations and method

One data layer covering churches/mosques/synagogues together (christian=3053, muslim=246, jewish=114), not three separate fetches — each feature carries its OSM `religion` tag as a property so the frontend can sub-filter which religion's points are shown from this one dataset. Only religion=christian/muslim/jewish are fetched; other religions (buddhist, hindu, sikh, etc.) are out of scope for this layer. Centroid only (out center;), Greater London bbox. Radius buffers are precomputed both for this combined dataset (this manifest entry's own buffer files) and separately per religion (see the places_of_worship_christian/_muslim/_jewish manifest entries) — the UI's per-religion checkboxes each need their own radius circles.

Open Database License (ODbL) v1.0 — https://www.openstreetmap.org/copyright 2026-08-24
pollution_no2 Defra UK-AIR PCM modelled background pollution data — NO2 2024 annual mean
Limitations and method

PCM 2024 annual-mean modelled background NO2, µg/m³, native 1x1 km OSGB grid. Cells are clipped to the dissolved Greater London borough boundary and converted to WGS84. Background conditions only: excludes separately modelled roadside concentrations and must not be interpreted as address-level exposure or regulatory compliance. This is the first pollutant; a second can follow after the layer ships cleanly.

Open Government Licence (OGL) — © Crown copyright Defra via uk-air.defra.gov.uk 2026-09-01
population_density ONS LSOA population density workbook, Mid-2024 (https://www.ons.gov.uk/file?uri=/peoplepopulationandcommunity/populationandmigration/populationestimates/datasets/lowersuperoutputareapopulationdensity/mid2022revisednov2025tomid2024/sapelsoapopulationdensity20222024.xlsx); 2021 LSOA boundaries from ONS Open Geography Portal (https://services1.arcgis.com/ESMARspQHYMw9BZ9/arcgis/rest/services/Lower_layer_Super_Output_Areas_December_2021_Boundaries_EW_BSC_V4/FeatureServer/0/query), filtered by LAD 2023 Code prefix E09 and joined on LSOA21 code
Limitations and method

Mid-2024 population and people per km²; matching 2021 LSOA boundaries reprojected EPSG:27700 to EPSG:4326.

Open Government Licence v3.0 — https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/ 2026-09-20
pubs OpenStreetMap contributors, via Overpass API (amenity=pub)
Limitations and method

Greater London bbox; nodes/ways/relations represented by one centroid using Overpass out center;. Name/brand/addr:*, normalized opening_hours/phone/website and OSM provenance are stored. Clustered at wider zooms. Optional detail coverage in this snapshot: website 1787/3477; OSM-listed opening hours 1025/3477; phone 867/3477; human-readable brand 681/3477. Missing optional fields are omitted, never shown as unavailable.

Open Database License (ODbL) v1.0 — https://www.openstreetmap.org/copyright 2026-09-03
rail_noise Greater London Authority, London Datastore, 'Noise Pollution in London' — Rail_Lden_London.zip (Lden metric, pre-clipped to Greater London), originally DEFRA/Environment Agency strategic noise mapping data, data period Jan-Dec 2012 — https://data.london.gov.uk/download/2zwnk/dea27a3b-b394-48ed-8b57-0c0d9a84a4fc/Rail_Lden_London.zip — dataset page: https://data.london.gov.uk/dataset/noise-pollution-in-london-2zwnk
Limitations and method

Lden (24hr annual average, evening/night weighted) chosen as the primary noise metric over the LAeq,16h alternative also shipped in this dataset — Lden is the standard single-figure 'how noisy is this generally' indicator, more representative than a daytime-only figure. KNOWN LIMITATION: source data period is Jan-Dec 2012 — the London Datastore page has had no newer round published since (confirmed by checking the page directly); DEFRA's own national strategic noise maps have since moved to 2017/2022 rounds, but those aren't pre-clipped to Greater London the way this dataset is, and re-clipping the national dataset from scratch was explicitly out of scope for this layer. Review cycle set shorter than static boundary layers regardless, so a newer London Datastore round (if one appears) doesn't sit unnoticed. 5 raw NoiseClass bands (55.0-59.9 / 60.0-64.9 / 65.0-69.9 / 70.0-74.9 / >=75.0), each originally tens of thousands of small individually-mapped polygons, unioned per band then combined into 5 CUMULATIVE bands (75/70/65/60/55 dB-and-above) so each lower threshold fully contains every higher one — same nested 'darker = louder' convention as heathrow_noise, relying on the same cumulative-opacity Leaflet stacking (see app/static/js/layers.js). Simplified to ~30m tolerance in EPSG:27700 (the source CRS) — deliberately coarser than this project's usual ~8-10m polygon-simplify range, because this shape (the outline of everywhere near enough to any road/rail line across all of Greater London) traces a sprawling, branching network, categorically unlike a single park or noise-contour footprint; fine for general noise awareness, not property-line accuracy (same caveat heathrow_noise's own notes make). ALSO drops individual disjoint PARTS below 2000sqm and small interior HOLES below 20000sqm — found necessary from the real output in two stages, not assumed upfront. First (parts): the first full build produced a 164MB road_noise.geojson (163.9MB/42.8MB road/rail) because the loudest cumulative band alone had 81,634 disjoint parts, 97.7% of them under 2,000sqm and together just ~3% of that band's total area (median part size: 50sqm) — dropping them cut the file to 48MB/12.6MB, still far too large. Second (holes, the fix that actually mattered): even at 100-250m tolerance the same band still had ~370k vertices, barely down from ~436k at 10-20m — the real bottleneck was 84,695 interior holes across those same 1,846 already-part-filtered polygons (preserve_topology=True must protect vertices in every hole to stay valid, so no tolerance could meaningfully reduce it); dropping small holes FIRST, before simplify, cut that band to ~53k vertices at 30m — this was never a tolerance problem. Both filters are the same underlying judgement call as green_spaces' small-unnamed-feature area filter (config.GREEN_SPACE_MIN_AREA_SQM, same 2,000sqm value for parts) applied to pieces/gaps of one already-unioned feature rather than whole named/unnamed features; `.buffer(0)` validity-repair runs after both hole-dropping and simplify, since either can occasionally produce a geometry GEOS considers invalid on input this complex. Then reprojected to WGS84. No radius buffer options — same reasoning as heathrow_noise: a distance buffer around a noise contour isn't a meaningful concept (see NO_RADIUS_BUFFER_LAYERS, app/services/layers.py). Reference/awareness layer only — display, not scoring input (Phase 1 scope).

Open Government Licence v3.0 — https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/ 2026-08-26
road_noise Greater London Authority, London Datastore, 'Noise Pollution in London' — Road_Lden_London.zip (Lden metric, pre-clipped to Greater London), originally DEFRA/Environment Agency strategic noise mapping data, data period Jan-Dec 2012 — https://data.london.gov.uk/download/2zwnk/4d54c645-60dc-406f-a5f1-8a8f6e3c1776/Road_Lden_London.zip — dataset page: https://data.london.gov.uk/dataset/noise-pollution-in-london-2zwnk
Limitations and method

Lden (24hr annual average, evening/night weighted) chosen as the primary noise metric over the LAeq,16h alternative also shipped in this dataset — Lden is the standard single-figure 'how noisy is this generally' indicator, more representative than a daytime-only figure. KNOWN LIMITATION: source data period is Jan-Dec 2012 — the London Datastore page has had no newer round published since (confirmed by checking the page directly); DEFRA's own national strategic noise maps have since moved to 2017/2022 rounds, but those aren't pre-clipped to Greater London the way this dataset is, and re-clipping the national dataset from scratch was explicitly out of scope for this layer. Review cycle set shorter than static boundary layers regardless, so a newer London Datastore round (if one appears) doesn't sit unnoticed. 5 raw NoiseClass bands (55.0-59.9 / 60.0-64.9 / 65.0-69.9 / 70.0-74.9 / >=75.0), each originally tens of thousands of small individually-mapped polygons, unioned per band then combined into 5 CUMULATIVE bands (75/70/65/60/55 dB-and-above) so each lower threshold fully contains every higher one — same nested 'darker = louder' convention as heathrow_noise, relying on the same cumulative-opacity Leaflet stacking (see app/static/js/layers.js). Simplified to ~30m tolerance in EPSG:27700 (the source CRS) — deliberately coarser than this project's usual ~8-10m polygon-simplify range, because this shape (the outline of everywhere near enough to any road/rail line across all of Greater London) traces a sprawling, branching network, categorically unlike a single park or noise-contour footprint; fine for general noise awareness, not property-line accuracy (same caveat heathrow_noise's own notes make). ALSO drops individual disjoint PARTS below 2000sqm and small interior HOLES below 20000sqm — found necessary from the real output in two stages, not assumed upfront. First (parts): the first full build produced a 164MB road_noise.geojson (163.9MB/42.8MB road/rail) because the loudest cumulative band alone had 81,634 disjoint parts, 97.7% of them under 2,000sqm and together just ~3% of that band's total area (median part size: 50sqm) — dropping them cut the file to 48MB/12.6MB, still far too large. Second (holes, the fix that actually mattered): even at 100-250m tolerance the same band still had ~370k vertices, barely down from ~436k at 10-20m — the real bottleneck was 84,695 interior holes across those same 1,846 already-part-filtered polygons (preserve_topology=True must protect vertices in every hole to stay valid, so no tolerance could meaningfully reduce it); dropping small holes FIRST, before simplify, cut that band to ~53k vertices at 30m — this was never a tolerance problem. Both filters are the same underlying judgement call as green_spaces' small-unnamed-feature area filter (config.GREEN_SPACE_MIN_AREA_SQM, same 2,000sqm value for parts) applied to pieces/gaps of one already-unioned feature rather than whole named/unnamed features; `.buffer(0)` validity-repair runs after both hole-dropping and simplify, since either can occasionally produce a geometry GEOS considers invalid on input this complex. Then reprojected to WGS84. No radius buffer options — same reasoning as heathrow_noise: a distance buffer around a noise contour isn't a meaningful concept (see NO_RADIUS_BUFFER_LAYERS, app/services/layers.py). Reference/awareness layer only — display, not scoring input (Phase 1 scope).

Open Government Licence v3.0 — https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/ 2026-08-26
rowing_clubs OpenStreetMap contributors via Overpass API (sport=rowing OR club=rowing), Greater London bbox
Limitations and method

Node/way/relation results, centroid-only via out center; deduplicated by OSM type and id when both predicates match.

Open Database License (ODbL) v1.0 — https://www.openstreetmap.org/copyright 2026-09-20
sainsburys OpenStreetMap contributors, via Overpass API (shop=supermarket, brand=Sainsbury's)
Limitations and method

Classified from a single shop=supermarket query (Overpass out center;) — see scripts/fetch_supermarkets.py's module docstring for the matching heuristic (brand tag, falling back to brand:wikidata only when brand is absent; never guessed from the freeform name tag). Of 1371 shop=supermarket features found in the bbox: 170 carry a recognized brand that isn't one of these 9 tracked chains (Amazon Fresh, Best Foods, Budgens, Costcutter, Eataly, H Mart, Jumbo, Londis, Longdan, Lycamobile, Market Basket, Mega Saver, Nisa, Nisa Extra, Nisa Local, Planet Organic, Spar, The Food Warehouse, The Source Bulk Foods, Waitrose, Waitrose & Partners, Whole Foods Market, Wholefoods Market), and 550 have no usable brand tag at all (neither brand nor brand:wikidata) — logged during fetch, not silently dropped and not assigned a brand by guessing. Centroid only (out center;), Greater London bbox. Waitrose is fetched separately by scripts/fetch_waitrose.py (matched by name, predates this script).

Open Database License (ODbL) v1.0 — https://www.openstreetmap.org/copyright 2026-08-24
schools DfE Get Information about Schools (GIAS), establishment fields extract (https://get-information-schools.service.gov.uk/Downloads); Ofsted monthly management information, latest inspections (as at 2026-07-31), via gov.uk's content API
Limitations and method

3060 open London schools (Academies/Free Schools/LA-maintained/Independent/Special), joined to Ofsted by URN. GIAS no longer carries any Ofsted field itself (removed Jan 2025) — ratings come entirely from Ofsted's own separate monthly extract. Ofsted's real current data tracks THREE inspection threads per school (latest full inspection — report-card OR old-framework, whichever actually applies — latest OEIF-graded inspection specifically, and latest ungraded/monitoring visit); see classify_ofsted_rating() in the fetch script and CLAUDE.md for the full normalisation logic. State/Grammar/Private/Faith/SEN filter buckets are NOT mutually exclusive (a state grammar school matches both) except State/Private themselves — see classify_school_type() and CLAUDE.md.

Open Government Licence v3.0 (GIAS: Crown copyright, DfE; Ofsted: Crown copyright, OGL v3.0) — https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/ 2026-08-27
shopping_malls OpenStreetMap contributors, via Overpass API (shop=mall)
Limitations and method

Greater London bbox; nodes/ways/relations represented by one centroid using Overpass out center;. Name/brand/addr:*, normalized opening_hours/website and OSM provenance are stored. Plain markers. Optional detail coverage in this snapshot: website 31/97; OSM-listed opening hours 19/97; human-readable brand 3/97. Missing optional fields are omitted, never shown as unavailable.

Open Database License (ODbL) v1.0 — https://www.openstreetmap.org/copyright 2026-09-03
social_rented ONS Census 2021, TS054 (Tenure of household), LSOA-level bulk CSV — https://www.nomisweb.co.uk/output/census/2021/census2021-ts054.zip (census2021-ts054-lsoa.csv). social_rented_pct = 100 × ('Social rented: Rents from council or Local Authority' + 'Social rented: Other social rented') / 'Total: All households' — ONS's own guidance is to add these two categories together since respondents confuse them. Enriches the existing London LSOA polygons built by fetch_population_density.py rather than duplicating them — see docs/decisions/social-rented-households.md.
Limitations and method

social_rented_pct field added to population_density.geojson's existing features (shared geometry, not a separate file). Household tenure, not dwelling ownership/status. Re-running fetch_population_density.py overwrites this file from scratch — rerun this script afterward to restore this field.

Open Government Licence v3.0 — https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/ 2026-09-20
tennis_courts OpenStreetMap contributors via Overpass API (sport=tennis on a physical leisure facility), fetched by Greater London bbox then clipped to the authoritative borough boundary
Limitations and method

sport=tennis; leisure in pitch, practice_pitch, sports_centre, sports_hall, stadium. Nodes/ways/relations become representative points via out center; 714 bbox result(s) outside Greater London removed; deduplicated by OSM type/id. access=private and access=no excluded; missing access retained as unknown (2936/3070), never claimed public. Name, operator, access, fee, lit and surface retained when tagged. Clustered below zoom 13 and individual at zoom 13+.

Open Database License (ODbL) v1.0 — https://www.openstreetmap.org/copyright 2026-09-30
tesco OpenStreetMap contributors, via Overpass API (shop=supermarket, brand=Tesco)
Limitations and method

Classified from a single shop=supermarket query (Overpass out center;) — see scripts/fetch_supermarkets.py's module docstring for the matching heuristic (brand tag, falling back to brand:wikidata only when brand is absent; never guessed from the freeform name tag). Of 1371 shop=supermarket features found in the bbox: 170 carry a recognized brand that isn't one of these 9 tracked chains (Amazon Fresh, Best Foods, Budgens, Costcutter, Eataly, H Mart, Jumbo, Londis, Longdan, Lycamobile, Market Basket, Mega Saver, Nisa, Nisa Extra, Nisa Local, Planet Organic, Spar, The Food Warehouse, The Source Bulk Foods, Waitrose, Waitrose & Partners, Whole Foods Market, Wholefoods Market), and 550 have no usable brand tag at all (neither brand nor brand:wikidata) — logged during fetch, not silently dropped and not assigned a brand by guessing. Centroid only (out center;), Greater London bbox. Waitrose is fetched separately by scripts/fetch_waitrose.py (matched by name, predates this script).

Open Database License (ODbL) v1.0 — https://www.openstreetmap.org/copyright 2026-08-24
ulez Transport for London, GIS Open Data Hub, 'Ultra Low Emission Zone (ULEZ)' — https://services1.arcgis.com/YswvgzOodUvqkoCN/arcgis/rest/services/Ultra_Low_Emission_Zone/FeatureServer/0/query — dataset page: https://gis-tfl.opendata.arcgis.com/datasets/TfL::ultra-low-emission-zone-ulez
Limitations and method

August 2023 expansion boundary (current) — the dataset's own description confirms this, and it's independently verified here: the unioned area of all 22 source polygons is ~1528 sq km, matching Greater London's real area (~1,572 sq km) almost exactly. KNOWN DATA QUIRK, checked not assumed: every source feature's own DATASET attribute reads 'Low Emission Zone' (a different, older, less strict TfL scheme) rather than 'Ultra Low Emission Zone' — stale/inherited metadata on TfL's part, not a sign this is the wrong layer (the area check above is the real evidence). 22 source polygons unioned into one (Multi)Polygon boundary via plain unary_union (small enough that coverage_union_all's speed advantage, needed for road/rail noise's tens-of-thousands-of-polygons case, doesn't matter here). Simplified to ~10m tolerance in EPSG:27700 (this project's usual range). No TfL branding (no roundel, no TfL line-colour palette) — outline only, same rule already applied to the Underground/Overground marker badges elsewhere in this project. No radius buffer options — a reference/boundary layer, not a proximity POI layer. Reference/awareness layer only — display, not a Phase 2 scoring input.

Open Government Licence v3.0 — https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/. TfL Open Data also asks for "Powered by TfL Open Data" attribution — added to the map's own attribution control (app/static/js/map-init.js) alongside cycle_lanes, covering all TfL-sourced layers with one shared line. Retroactive fix — this was missed when ulez was first built; caught while adding cycle_lanes. (cycle_parking, also TfL-sourced, was removed in a later bug-fix pass.) 2026-08-26
underground OpenStreetMap contributors, via Overpass API (railway=station/halt, classified as London Underground)
Limitations and method

Split from a single railway=station/halt query (Overpass out center;) via tags already present in the data — see scripts/fetch_stations.py's module docstring for the exact heuristic. 41 station(s) are genuine interchanges and appear in BOTH this layer and its counterpart (e.g. Bank, Farringdon, Highbury & Islington) — an interchange should count for proximity to each mode it serves, not be forced into one bucket. 11 station(s) excluded from both layers as heritage/miniature railways (usage=tourism), not real transit: 'Willow Lawn', 'Woody Bay', 'Cassiobury Park Station', 'Haste Hill', 'Depot Approach', 'Ealing End', 'Swanley New Barn Miniature Railway', 'Hanworth Halt', "Eddie's Rest", None, 'Barking Park'. Ambiguous case resolved during development: secondary OSM nodes at Liverpool Street and Paddington carry only operator="Transport for London" (no station=subway or network=London Underground tag) — added as a third Underground signal after finding these. Greater London bbox, centroid only (out center;), like every POI layer except green_spaces.

Open Database License (ODbL) v1.0 — https://www.openstreetmap.org/copyright 2026-08-21
underground_lines OpenStreetMap, via Overpass API — route_master=subway relations (network=London Underground) recursed down to their member route relations' own way geometry, deduplicated at the way level.
Limitations and method

All 11 current Underground lines, each its own MultiLineString feature (one component per unique OSM way — real traced track shape, not straight lines between stations) in that line's own official TfL digital-standard colour (cross-checked against two independent sources, matching exactly — see this script's own module docstring). Route geometry from OpenStreetMap's own railway route relations via Overpass (NOT TfL's Route/Sequence API any more — that turned out to be essentially straight lines between consecutive station stops, not real track shape; see this script's own module docstring for the full story), traced once and cached as a static file. No radius buffer — a transit line has no meaningful 'buffer around a route' concept.

Open Database License (ODbL) v1.0 — https://www.openstreetmap.org/copyright 2026-08-28
waitrose OpenStreetMap contributors, via Overpass API (shop=supermarket, name~"Waitrose")
Limitations and method

Matched by name (case-insensitive) rather than brand tag alone, since brand tagging is inconsistent in OSM. Centroid only (out center;), Greater London bbox.

Open Database License (ODbL) v1.0 — https://www.openstreetmap.org/copyright 2026-08-21

Map: OpenFreeMap / OpenMapTiles, using data © OpenStreetMap contributors. Commute calculations: Powered by Geoapify.

Limitations and disclaimer

Information can be incomplete or outdated, and local geocoding may resolve to an approximate street or place rather than a building entrance. Recorded crime is not a prediction of individual risk. Socioeconomic statistics describe areas, not individuals. Noise and environmental layers are modelled at their stated resolution.

OLL is for general information and is not legal, financial, medical, surveying or other professional advice. Verify important facts with the responsible authority and visit a location yourself before making a decision.