feat(geo): adopt Mapbox building calibration (3.1 m per level) (#26)

Merged PR #26: adopt Mapbox building calibration

Co-authored-by: Zwitschi <zwitschi82@gmail.com>
This commit was merged in pull request #26.
This commit is contained in:
zwitschi authored and zwitschi committed 2026-11-08 16:38:28 +01:00
1 parent 81f7cd3991
commit 5ffe62f6aa
37 files changed
+125 -88

No files matched your search

+33 -6
View File
@@ -4,9 +4,21 @@
Usage:
Geo/tools/block_height_report.py Geo/raw/osm-api-map-2026-11-06.osm
`height` and `building:levels` are the only height evidence OSM carries. Where
neither exists, Blosm falls back to `defaultLevels * levelHeight`, which is a
placeholder: the demo renders a constant-height block instead of a real skyline.
Height evidence, in priority order:
1. the OSM `height` tag (a direct measurement);
2. an explicit override in `HEIGHT_OVERRIDES` (currently Mapbox-derived — see below);
3. `building:levels` x `LEVEL_HEIGHT`;
4. a flagged placeholder guess.
`LEVEL_HEIGHT` is 3.1 m, not the 3.2 m first assumed: cross-checking every block
building against Mapbox's building data (`Geo/tools/mapbox_buildings.py`) showed
our `levels x 3.2 m` figure running 0.6 m high per six levels, i.e. 3.1 m per
level. That calibration was adopted on 2026-11-09.
`HEIGHT_OVERRIDES` carries the few buildings where Mapbox disagrees with a guess;
each entry records why, so the number is not orphaned from its evidence.
This tool lists every building in the demo bounds with its height source, so the
substitutions are visible and measurable rather than buried in the model.
@@ -19,10 +31,22 @@ import sys
import osm_common as oc
LEVEL_HEIGHT = 3.2
LEVEL_HEIGHT = 3.1
# Per-way height overrides, applied after the OSM height tag and before the
# levels/guess derivation. Keep `reason` short but specific: a height without its
# evidence is a guess wearing a costume.
HEIGHT_OVERRIDES = {
# Rear outbuildings tagged only `building=yes`/`apartments`; the two-level
# fallback rendered them as 6.4 m blocks. Mapbox's building layer gives 3.0 m
# for the same footprints (single storey plus parapet), adopted 2026-11-09.
1490910971: (3.0, "Mapbox building layer: 3.0 m (single storey)"),
1492058875: (3.0, "Mapbox building layer: 3.0 m (single storey)"),
1492747608: (3.0, "Mapbox building layer: 3.0 m (single storey)"),
}
def classify_height(tags, level_height=LEVEL_HEIGHT):
def classify_height(tags, level_height=LEVEL_HEIGHT, way_id=None):
"""Return (height_m, source) for one OSM way's tags.
Extracted from `main()` so the classification is testable: the roof/shelter
@@ -39,6 +63,9 @@ def classify_height(tags, level_height=LEVEL_HEIGHT):
return 0.0, f"not a building volume ({kind})"
if tags.get("height"):
return float(tags["height"]), "height tag"
if way_id is not None and int(way_id) in HEIGHT_OVERRIDES:
height, reason = HEIGHT_OVERRIDES[int(way_id)]
return height, f"override: {reason}"
if tags.get("building:levels"):
return (float(tags["building:levels"]) * level_height,
f"levels x {level_height} m")
@@ -66,7 +93,7 @@ def main():
pts = oc.way_points(nodes, refs)
if not pts or not in_block(pts):
continue
height, source = classify_height(tags)
height, source = classify_height(tags, way_id=wid)
nearest = min(oc.project(p, line, lat0, lon0, lengths)[0] for p in pts)
side = "S" if oc.project(pts[0], line, lat0, lon0, lengths)[2] < 0 else "N"
rows.append((side, nearest, wid, tags.get("name", "") or tags.get("addr:housenumber", ""),
+1 -1
View File
@@ -184,7 +184,7 @@ def build_rows(nodes, ways, token, radius, max_distance, delay) -> list[dict]:
lat, lon = centroid_lat_lon(points)
payload, cached = tilequery(lat, lon, token, radius, delay)
feature = nearest_building(payload, max_distance)
ours_height, ours_source = bhr.classify_height(tags)
ours_height, ours_source = bhr.classify_height(tags, way_id=wid)
theirs = None if feature is None else feature.get("height")
verdict, delta = compare(ours_height, ours_source, theirs)
rows.append({