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>
91 lines
5.0 KiB
Markdown
91 lines
5.0 KiB
Markdown
# 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 (Douglas–Peucker) — 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).
|