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

Owner decision: adopt the two findings from the Mapbox cross-check.

- LEVEL_HEIGHT 3.2 -> 3.1 m in block_height_report.py and the Blosm
  --level-height default; every levels-derived building drops ~0.6 m per 6
  levels, matching Mapbox.
- HEIGHT_OVERRIDES for the three rear building=yes/apartments structures that
  Mapbox gives as single storey (3.0 m) where our 2-level fallback rendered
  6.4 m blocks.
- classify_height() takes an optional way_id so overrides apply; the Mapbox
  cross-check passes it and now reports 66 match / 6 non-building / 0 disagree.
- Geometry follows: blockout regenerated at 3.1 (frontage object names
  preserved, 6/6 exported), frontage re-imported, DemoMap rebuilt, packaged
  -qaselftest PASS (walk, reset, pause, transect 0 holes, wall probe 0/35).
- Docs: Art/blender/README scale table + a note that Blosm adds ~1.9 m roof mass
  above levels x levelHeight; TODO item closed; MEMORY records the gotcha that
  blockout regeneration must re-verify the element.* frontage mapping.
This commit is contained in:
zwitschi committed 2026-11-08 16:38:04 +01:00
1 parent 81f7cd3991
commit 1b22d551fe
37 files changed
+126 -64

No files matched your search

+14 -2
View File
@@ -146,13 +146,25 @@ Blender works in metres, and the imported meshes match the OSM geometry, so the
| Building | Blender dimensions | OSM way outline | Height source |
| --- | --- | --- | --- |
| El Capitan Entertainment Centre | 25.72 × 54.47 × 14.50 m | 25.75 × 54.88 m | `height` tag 14.5 |
| El Capitan Theatre | 38.66 × 54.46 × 20.96 m | 38.59 × 54.87 m | `building:levels=6` × 3.2 m, plus roof |
| El Capitan Theatre | 38.66 × 54.46 × 20.55 m | 38.59 × 54.87 m | `building:levels=6` × **3.1 m**, plus ~1.9 m roof |
| Madame Tussaud's | 42.48 × 56.77 × 15.90 m | 42.0 m frontage | `height` tag 15.9 |
| TCL Chinese Theatre | 43.78 × 75.90 × 18.10 m | 44.0 m frontage | `height` tag 18.1 |
Widths agree to a few centimetres; depths differ by up to 0.4 m because Blosm assembles **multipolygon relations**, not the raw way I measured. Placement after the frame correction agrees with the UTM control points to 0.1–0.5 m per building — the residual is relation assembly and Blosm's edge straightening, not projection.
Buildings **without** a `height` tag get levels × 3.2 m, which is a placeholder, not evidence. Every such height is a modelling decision to be replaced from the reference photography.
Buildings **without** a `height` tag get levels × **3.1 m**. The storey height was
3.2 m until 2026-11-09, when a cross-check of every block building against
Mapbox's building data (`Geo/tools/mapbox_buildings.py`) showed our figure
running 0.6 m high per six levels — i.e. 3.1 m per level — and that calibration
was adopted. The few buildings where Mapbox disagreed with a pure
`levels × 3.1 m` fallback carry an explicit override in
`Geo/tools/block_height_report.py::HEIGHT_OVERRIDES`.
Blosm adds roof mass on top of `levels × levelHeight`: the two `levels`-derived
frontage buildings both land ~1.9 m above their table height (6922 39.1 m against
37.2 m, El Capitan Theatre 20.5 m against 18.6 m). That offset is the roof, not a
height error — the table records the tagged height, the mesh records the storey
plus its roof.
## Relation completeness
Binary file not shown.
+3 -1
View File
@@ -40,7 +40,9 @@ def parse_args():
ap.add_argument("--osm", required=True, help="local .osm file to import")
ap.add_argument("--bounds", required=True, help="south,west,north,east in WGS84 degrees")
ap.add_argument("--out", required=True, help="output .blend path")
ap.add_argument("--level-height", type=float, default=3.2, help="metres per building level")
# 3.1 m, not 3.2: calibrated against Mapbox building data on 2026-11-09
# (see Geo/tools/mapbox_buildings.py and Geo/tools/block_height_report.py).
ap.add_argument("--level-height", type=float, default=3.1, help="metres per building level")
ap.add_argument("--origin", default=None,
help="lat,lon to use as the Blender origin (project origin). Default: Blosm centres on the extract")
ap.add_argument("--no-subdivide", action="store_true", help="skip OSM area subdivision")
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+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({
+21
View File
@@ -379,3 +379,24 @@ Scope: facts and traps that cost time to establish. Planned work lives in [TODO.
**0.6 m lower per 6 levels** → Mapbox implies **3.1 m per level**, not 3.2.
Worth revisiting `LEVEL_HEIGHT` if a measured storey height ever lands.
- Its query API surface: `height`, `min_height`, `type`, `extrude` per polygon.
## 2026-11-09 — Mapbox calibration adopted into the blockout
- Owner decision: adopt Mapbox. Applied:
- `LEVEL_HEIGHT` 3.2 → **3.1 m** in `Geo/tools/block_height_report.py`, and the
Blosm `--level-height` default in `Art/blender/blockout/blosm_blockout.py`.
- `HEIGHT_OVERRIDES` in block_height_report: ways 1490910971, 1492058875,
1492747608 → 3.0 m (Mapbox single-storey; the 2-level guess gave 6.4 m).
- `classify_height(tags, level_height, way_id)` now takes the way id so
overrides apply; `mapbox_buildings.py` passes it, so the cross-check agrees
with the height table.
- Re-running the cross-check: **66 match, 6 non-building, 0 close/disagree**.
- Geometry: blockout regenerated at 3.1 (object names preserved — the frontage
exporter maps `element.020`/`element`/`element.019`/named buildings, all still
present, 6/6 exported), frontage re-imported, DemoMap rebuilt, packaged build
`-qaselftest` all PASS.
- **Blosm adds roof mass** on top of `levels × levelHeight`: 6922 39.1 m vs table
37.2, El Capitan Theatre 20.5 m vs 18.6 — both ~+1.9 m. That is a roof, not a
height error; the table is the tagged height, the mesh is storey + roof.
- Gotcha: regenerating the blockout is safe for names, but any future change to
the import must re-verify the 6/6 frontage export and the `element.*` mapping.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -15,7 +15,7 @@ Suggested blender skills: **references** (reference-image-match and visual-match
### Patch tasks - active - 01a1b26b-3c94-707f-9665-2f6b4a79b564 (branch `scale-facade-0.1.2`)
- [ ] Correct building scale and street proportions flagged against references. **Cross-check available:** `Geo/tools/mapbox_buildings.py` merges our OSM heights with Mapbox's building data for the same footprints (72 buildings: 58 match, 5 close, 3 disagree, 6 non-building). Three rear `building=yes`/`apartments` structures still render at our 2-level guess (6.4 m) against Mapbox's 3.0 m; and every `levels × 3.2 m` building is 0.6 m taller per 6 levels than Mapbox implies, i.e. **Mapbox suggests 3.1 m per level**. Neither is applied yet — decide from the reference photography.
- [x] Correct building scale and street proportions flagged against references. **Mapbox adopted 2026-11-09.** `Geo/tools/mapbox_buildings.py` merges our OSM heights with Mapbox's building data for the same footprints; after adopting its two findings the cross-check reports **66 match, 6 non-building, 0 disagreements**. Adopted: **`LEVEL_HEIGHT` 3.2 → 3.1 m** (`block_height_report.py`, and the Blosm `--level-height` default), plus explicit `HEIGHT_OVERRIDES` (3.0 m) for the three rear `building=yes`/`apartments` structures Mapbox gives as single-storey. Blockout regenerated, frontage re-exported/re-imported, DemoMap rebuilt; packaged `-qaselftest` all PASS. Measured effect: 6922 42.1 → 39.1 m, El Capitan Theatre 20.0 → 20.5 m (both now ~1.9 m above their table height = roof mass).
- [ ] Refine hero façade details where reference photography shows discrepancies.
- [x] **Replace the placeholder heights.** **Resolved 2026-11-08 — they were never unknown buildings.** The three "guessed" ways are not building volumes at all: `1360755125` and `1360755126` are `building=roof` at `layer=1` (elevated canopies) and `1417995292` is `amenity=shelter` / `shelter_type=public_transport` (a transit shelter). The report defaulted them to "2 levels × 3.2 m" and produced the bogus 6.4 m. `Geo/tools/block_height_report.py` now classifies roof/shelter explicitly and reports **0 guessed heights**: all 15 block volumes have a real `height` tag, a `building:levels` derivation, or are correctly excluded as non-building volumes. No photograph measurement was needed, and no frontage building was affected.
- [ ] Resolve the remaining unknown on the hero: the colonnade currently occupies about 45 % of the height against roughly 50–60 % in the photographs. Fix the vertical proportions. **Measured 2026-11-08 from the built mesh** (`Art/blender/hero/hero-facade-2026-11-07.blend`): envelope 0–14.50 m; stylobate 0–2.00; columns 2.00–7.95; capitals 7.95–8.90; entablature 8.90–12.30; parapet 12.30–14.50. So the colonnade (columns + capitals) is **6.90 m = 47.6 %** of the height — inside the range only at the low end. Bringing it to ~55 % needs ~+1.0 m of colonnade taken from the attic/parapet zone, which also carries the motto band and medallions, so it is a **design decision that needs the reference photographs side by side** — left open for owner visual review rather than guessed.