diff --git a/docs/A-progress.md b/docs/A-progress.md index 0e19219..6c6a8d2 100644 --- a/docs/A-progress.md +++ b/docs/A-progress.md @@ -1,11 +1,90 @@ # LANE A — CITYGEN · progress (PROCITY-A) -*Status: **all deliverables landed; self-check ALL GREEN (157,407/157,407); rounds 2–30 + 37 closed**. +*Status: **all deliverables landed; self-check ALL GREEN (157,647/157,647); rounds 2–30 + 37 + 39 closed**. +R39 (v9 item 39.1): THE ADDRESS LAYER — 17,835 named ways stop being dropped on the floor. New pure +module `address.js`; **1,192/1,219 corpus shops (97.8%) resolve to a real street name**, 0 wrong names +on 884 shop-bearing edges against an independent resolver, **all goldens frozen, zero re-pins.** R37 (v8 wave 0 §0.1): the poster-clearance gate was VACUOUS — it validated my own placer against my own law and stayed green while all 14 posters on the default boot stood on B's bitumen. Corrected to three arms with a permanent control; proven to fail on the pre-ruling geometry where the old gate cannot. All goldens frozen, zero re-pins.* +## Round 39 (2026-08-03) — v9 item 39.1: THE ADDRESS LAYER (the approved thing; B and C consume it) + +**The full contract, per-town table and reasoning live in `LANE_A_NOTES.md §39`** — published in the +first commit, because B (39.2 the fog) and C (39.3 the op-shop dig) build against it in parallel. + +**Fable's three binding facts, re-derived independently — all three land exactly.** 19,132 road ways, +**17,835 named (93.2%)**; median plan-edge-sample → nearest named way **0.03–0.90 m** (worst katoomba); +**1,192 / 1,219 corpus shops (97.8%)** resolve. Nothing to contradict. + +**What landed.** `web/js/citygen/address.js` — pure, **zero imports**, no THREE/fetch/DOM/module state, +no plan mutation. `createAddresses(plan, cache|null)` → `streetOf` · `localityOf` · `cohort` · +`streets` · `stats`. Two suppliers, one field: a cache town fills `label` from real named ways, the +synthetic from `district.kind` + block. Plus `getTownCache(key)` (additive, read-only) because +`index.html:129` registers the cache and drops its reference, so `roads[]` was unreachable downstream. +`map.html` now loads real caches and prints the address — verified in-browser on castlemaine (*"XXXX +Antiques — Elizabeth Street · south side"*) and on the synthetic (*"Nev's Pawn & Loan — the warehouse +fringe · north side"*) **through the identical line of code**. + +**Two departures from the synthesis, both measured.** + +1. **The shift is recovered EXACTLY, not voted for.** I implemented the proposed centroid-align + + nearest-waypoint vote first: it returns the **WRONG shift on 5 of 23 towns** (braddon, fremantle, + hobart, northbridge, westend) — island culling drags the plan centroid off the cache's and the vote + converges *confidently* on a wrong offset. A wrong shift renames every street in the town. Replaced + with a lattice intersection (node − waypoint candidates, filtered by further nodes): **exact on + 23/23, nothing to tune, returns null rather than a guess.** +2. **Five samples along an edge, not the midpoint.** A midpoint cannot distinguish a street from the + street that crosses it — at an intersection both are ~0 m away. + +**The one-line upgrade: TAKEN, and it is genuinely that clean.** `norm.shift = {shx, shz}` goes onto the +normalization log, reaching the caller only via the existing `opts.report` sink — never the plan object. +It is a **cross-check, not a dependency**: the selfcheck compares plan_osm's published shift against +address.js's independent recovery and fires on a **0.01 m** disagreement. Without it the recovery would +only be checkable against itself, which is the exact vacuous-gate species R37 was about. + +**The gates (+240 checks), each proven to fire on a broken world.** + +| arm | today | breakage that fires it | +|---|---|---| +| 0 wrong names on shop-bearing edges, vs an **independent way-membership resolver** | **857/857 agree, 0 wrong** | resolver or shift drift | +| corpus resolution ≥ 97% | 1,192/1,219 (97.8%) | names stripped → 0.0% | +| shift recovered == plan_osm's published shift | exact, 23/23 | a **0.01 m** error | +| `createAddresses` does not mutate the plan | byte-identical per town | any write to `plan` | +| CONTROL: `roads[].name` stripped (**the pre-change state**) | 0 streets, supplier `district` | — | +| CONTROL: no cache passed | supplier `district`, visible in `stats()` | — | +| all-edge disagreements **PINNED at 4** (not `> 0`) | 4/30,986 | a control drifting to zero | + +The 4 corpus disagreements are all 2D-stacked ways where the question has no single answer (geelong's +two adjacent malls; a motorway crossing *over* a street on glebe and redhill ×2) and **none carries a +shop** — which is why the shop-bearing arm is 0. + +**The tolerance finding, which is the one worth keeping.** Coverage rises with tolerance (2 m → 85.2%, +6 m → 97.4%, 8 m → 97.8%, 12 m → 99.1%) but **correctness saturates at 6 m**: 857 agreeing frontage +names at 6, 8, 12, 16 *and* 24 m. Every metre past 6 buys exactly one thing — a name attached to a way +OSM left unnamed (0 at 6 m, 4 at 8 m, 14 at 24 m). **It can never buy a correction.** Shipped at the +briefed 8 m, which keeps four borrowings, all arcade footways inside a named mall. + +**And the resolver degrades to null, not to a wrong name.** Feed it a shift wrong by 30 m and +castlemaine/katoomba/adelaide return **zero** street names rather than a town full of wrong ones. Feed +it one wrong by 3 m and adelaide's coverage *rises* from 89 to 98 shops while being wrong — which is +why the shift arm is exact-or-nothing and **coverage is never used as a proxy for correctness.** + +**The failure modes, named.** All 27 unresolved shops front a way OSM genuinely leaves unnamed: +adelaide 21 `arcade` (its arcades are the town's retail spine and OSM leaves the footways unnamed — it +is the corpus worst at 80.9% and it is *honestly* worst), fitzroy 1, geelong 1, launceston 1 + 2 `lane`, +northbridge 1. **`null` is the honest answer and the module returns it**; a shop with `street: null` +carries `label: null` too, asserted per town. + +**The synthetic's honest limit:** 12 label cells over 493 shops, median 41, max 147. Coarse — the same +structural fact §8 of the charter records. `block` is the finer cell (32 shop-bearing blocks, median 14). + +**GOLDENS: NOTHING MOVED, ZERO RE-PINS.** synthetic `0x5f76e76`, gig `0xec7a2d39`, 3 osm fixtures, 23 +`REAL_TOWN_GOLDENS`, 23 `REAL_TOWN_GIG_GOLDENS` — all untouched. Selfcheck **157,407 → 157,647 ALL +GREEN**. `createAddresses` costs **≤27 ms** on the worst real town (bendigo), **0.3 ms** on the +synthetic, once at plan time; zero draws, zero triangles, zero plan fields. + ## Round 37 (2026-08-03) — v8 WAVE 0 §0.1: THE KERB (ruling-side) — the gate I owned was the vacuous one Full detail in LANE_A_NOTES §37. Four jobs, all measured before they were written. diff --git a/docs/CITY_SPEC.md b/docs/CITY_SPEC.md index 7c66649..4e75064 100644 --- a/docs/CITY_SPEC.md +++ b/docs/CITY_SPEC.md @@ -572,6 +572,34 @@ nothing outside the cache-schema path). > count beats teleport.** The frozen set is unmoved by the entire epoch: synthetic `0x3fa36874`, gig > `0xb1d48ea1`, marched fixtures, classic. +### The address layer (v9, ROUND39 item 39.1) — DERIVED, never stored on the plan + +`roads[].name` has always ridden the v2 cache (**17,835 of 19,132 ways carry one — 93.2%**) and +`plan_osm.buildRealRoads` has always dropped it: `rawWays.push({ kind, pl })` keeps the kind and the +geometry and discards the name. **It still does, deliberately.** Street names are recovered +*downstream* by `web/js/citygen/address.js` — a pure module (zero imports) that reads a plan plus its +cache and returns a query object: + +```js +createAddresses(plan, cache|null) → streetOf(edgeId) · localityOf(shopId) · cohort(pred) · streets() · stats() +``` + +**THE RULE, and it is a schema rule, not a style note: no street name, district label or address ever +becomes a CityPlan field.** The plan is the frozen producer contract; addressing is derived data with a +97.8% resolution rate and a 2.2% honest-`null` tail, and baking either into `plan.lots`/`plan.shops` +would move all 46 pinned goldens to store something a consumer can compute in ≤27 ms. It is recoverable +because plan nodes ARE snapped projected way points (`SNAP=3`), so a plan edge lies **on** its way — +median sample-to-way distance **0.03–0.90 m** across the 23 caches. + +The one thing the lift now publishes is its own centring translation, `norm.shift = {shx, shz}`, which +reaches the caller **only through the existing `opts.report` sink and never through the plan object** — +so `JSON.stringify(plan)` is byte-identical and every golden is frozen by construction. It exists as a +**cross-check**: `address.js` re-derives the same shift independently, and the selfcheck compares the +two. Full contract, per-town rates and failure modes in `LANE_A_NOTES.md §39`. + +**Consumers must not branch on town type.** `localityOf(shopId).label` is a real street name on a cache +town and a district phrase (*"the market end"*) on the synthetic — one field, two suppliers, one contract. + ## The stock ladder (v5 — THE REAL SHOP; frozen at `v5.0`, ROUND27 ledger #6) > **v4 made the towns real; v5 makes the shops real.** The map-level contract above is unchanged by this diff --git a/web/map.html b/web/map.html index 47cfbc1..723ef72 100644 --- a/web/map.html +++ b/web/map.html @@ -86,7 +86,7 @@