PROCITY/web/assets/towns/README.md
m3ultra 4dc80c2146 Lane A round 18: town-cache schema v2 (roads[]) — published FIRST (v4.0-alpha REAL ROADS)
The half-day handshake so E's build_towns.py can fetch road geometry (ledger #1). Optional roads[]
on the town cache carries real OSM way geometry; plan_osm's graph lift (consuming it) follows in the
next commit. v1 caches (roads absent) stay valid — the marched fallback — so nothing shipped regresses.

- validateTownCache accepts schema procity-town-cache/1 AND /2; when roads[] is present each road must be
  { kind:string, pts:[[lat,lon],…] } with >=2 finite points (else rejected); <2 pts dropped, unmapped
  highway kind → 'side' (warnings). ROAD_KIND maps OSM highway=* → CityPlan edge kind (main/side/lane/arcade).
- Full v2 contract in web/assets/towns/README.md (the doc E builds to): shape, road validity, the kind map,
  the fidelity knob (charter risk #4), and the bbox-bound-the-roads rule.
- E's 5 existing v1 caches re-validate clean; selfcheck ALL GREEN.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 15:54:19 +10:00

91 lines
5.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Real town caches — the home (`web/assets/towns/`)
**Lane A owns the contract; Lane E fills this directory** (ROUND17 ledger #6, the real-map scout).
Each real Australian town is one file, `<key>.json`, in the **town-cache contract** shape — the processed
output of `pipeline/build_towns.py` (Overpass → dedupe/suburb/parody-name/seeded-hours). `plan_osm.js`
marches it exactly like the checked-in fixtures in `web/js/citygen/osm_fixture.js`; the scout proves the
**existing** CityPlan contract eats real data — no new plan fields, no new geometry.
## The contract (authoritative: `validateTownCache()` in `web/js/citygen/plan_osm.js`)
```jsonc
{
"schema": "procity-town-cache/1",
"key": "katoomba_real", // matches the filename; distinct from the hand-made fixture key
"town": "Katoomba", // display name
"source": "osm",
"license": "ODbL 1.0", // REQUIRED — OSM is ODbL
"attribution": "© OpenStreetMap contributors",
"generator": "pipeline/build_towns.py", // optional provenance
"fetchedAt": "2026-07-16", // optional
"center": { "lat": -33.71, "lon": 150.31 },// REQUIRED — the equirectangular projection origin
"bbox": { "minLat": …, "minLon": …, "maxLat": …, "maxLon": … }, // optional metadata
"counts": { "raw": 512, "shops": 96 }, // optional metadata
"shops": [
{ "id": 318855426, "name": "Canty's Bookshop", "type": "book", "lat": -35.32, "lon": 149.18, "suburb": "Fyshwick" }
// …
]
}
```
**Hard requirements** (else the cache is rejected and does not boot):
- `center.{lat,lon}` finite numbers.
- `shops` an array of **`MIN_TOWN_SHOPS` (6)** entries (up to 4 venues + the one openLate landmark + a spare).
- every shop has finite `lat`/`lon`.
**Absorbed as warnings** (still boots): a blank `name` defaults to the type label; an unknown `type` remaps
to `opshop`; duplicate ids; and — importantly — **a cache spanning > 5 km warns "likely more than one
town"**. A cache MUST be ONE compact town: an over-broad Overpass bbox marches into an unusable multi-km
strip (the R17 mega-strip risk — a whole region of 2,918 shops became an 11.6 km avenue). Bound the query.
`type` should be a registry `SHOP_TYPE` (`record`/`opshop`/`toy`/`book`/`video`/`pawn`/`milkbar`/`dept`/`stall`);
`build_towns.py` maps OSM `shop=*` tags to these (unknowns land on `opshop`).
## Schema v2 — `roads[]` (ROUND18, v4.0-alpha REAL ROADS)
A **v2** cache adds an optional `roads[]` — the town's real OSM street geometry. When present, `plan_osm`
builds the CityPlan street graph from the **real roads** and seats each shop on its nearest real edge (its
real street, real order, real side) instead of marching shops onto synthetic parallel avenues. **`roads`
absent (or `schema` `procity-town-cache/1`) ⇒ the marched fallback** — every shipped v1 cache stays valid.
```jsonc
{
"schema": "procity-town-cache/2",
// … all v1 fields (center, shops, license, …) …
"roads": [
{ "id": 12345678, // optional (OSM way id)
"kind": "primary", // REQUIRED — the OSM highway=* class (see the map below)
"name": "Katoomba Street", // optional
"pts": [ [-33.7175, 150.3110], [-33.7140, 150.3112], [-33.7100, 150.3115] ] } // REQUIRED — ≥2 [lat,lon]
// …
]
}
```
**Road validity** (a bad road is a hard error; a soft one is absorbed):
- each road is `{ kind: string, pts: [[lat,lon], …] }` with **≥ 2 finite `[lat,lon]` points** — else **rejected**.
- `< 2` points → the road is **dropped** (warning); an unmapped `kind` falls to `side` (warning).
**`kind` → CityPlan edge kind** (`ROAD_KIND` in `plan_osm.js`; the lift then promotes a shop-dense `side`
road to the `main` spine): `motorway`/`trunk`/`primary`/`secondary` → `main`; `tertiary`/`unclassified`/
`residential`/`living_street`/`road` → `side`; `service`/`track` → `lane`; `pedestrian`/`footway`/`path`/
`steps`/`cycleway` → `arcade`. **Fidelity (charter risk #4, A's knob):** ship the geometry roughly as OSM
has it; `plan_osm` simplifies (DouglasPeucker) — don't pre-decimate below ~1 point per 5 m.
**Bound the roads to the town** (the bbox law): fetch `highway=*` within the same per-town bbox as the shops
so the graph is the town's streets, not a region's.
## Provenance (Lane E)
OSM data is **ODbL**. Ship attribution with the data: this `README`, a `SOURCES.md` (E's licence table),
and the `license`/`attribution` fields on every cache. Cache the **raw** Overpass responses too so re-runs
never re-fetch.
## Wiring
- `plan_osm.generatePlanOSM(seed, key, { cache })` marches a cache directly; `registerTownCache(key, cache)`
adds it to the runtime registry so `osmTownKeys()` and `generatePlanFor(seed, 'osm', { town: key })` see it.
- `web/js/citygen/selfcheck.js` auto-loads every `*.json` here: validates it, runs the full structural +
gig suites across the hero seeds, and pins a per-town golden (unpinned → prints the value to paste).