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>
This commit is contained in:
parent
cf1334fc73
commit
4dc80c2146
@ -42,6 +42,40 @@ strip (the R17 mega-strip risk — a whole region of 2,918 shops became an 11.6
|
||||
`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),
|
||||
|
||||
@ -48,9 +48,21 @@ function signOf(name) {
|
||||
//
|
||||
// `type` SHOULD be a registry SHOP_TYPE; an unknown OSM kind remaps to 'opshop' (a warning, absorbed —
|
||||
// same as the fixtures). A blank name defaults to the type label. `suburb` is optional.
|
||||
export const TOWN_CACHE_SCHEMA = 'procity-town-cache/1';
|
||||
export const TOWN_CACHE_SCHEMA = 'procity-town-cache/2'; // v2 (ROUND18) adds optional roads[]
|
||||
const ACCEPTED_SCHEMAS = new Set(['procity-town-cache/1', 'procity-town-cache/2']); // v1 stays valid → marched
|
||||
export const MIN_TOWN_SHOPS = 6; // functional floor: up to 4 venues + the one openLate landmark + a spare
|
||||
export const MAX_TOWN_SPAN_M = 5000; // a cache should be ONE compact town, not a region (see R17 risk note)
|
||||
|
||||
// ── roads[] (schema v2, ROUND18) ─────────────────────────────────────────────────────────────────
|
||||
// OSM `highway=*` → CityPlan edge kind. The graph lift refines this with shop density (a `side` road
|
||||
// dense with shops becomes the `main` spine). Anything unmapped defaults to 'side'.
|
||||
export const ROAD_KIND = {
|
||||
motorway: 'main', trunk: 'main', primary: 'main', secondary: 'main',
|
||||
tertiary: 'side', unclassified: 'side', residential: 'side', living_street: 'side', road: 'side',
|
||||
service: 'lane', track: 'lane',
|
||||
pedestrian: 'arcade', footway: 'arcade', path: 'arcade', steps: 'arcade', cycleway: 'arcade',
|
||||
};
|
||||
export const roadEdgeKind = hwy => ROAD_KIND[hwy] || 'side';
|
||||
const isNum = v => typeof v === 'number' && Number.isFinite(v);
|
||||
|
||||
// Validate a cache against the contract. { ok, errors[], warnings[] } — errors mean plan_osm would not
|
||||
@ -58,7 +70,7 @@ const isNum = v => typeof v === 'number' && Number.isFinite(v);
|
||||
export function validateTownCache(cache) {
|
||||
const errors = [], warnings = [];
|
||||
if (!cache || typeof cache !== 'object') return { ok: false, errors: ['cache is not an object'], warnings };
|
||||
if (cache.schema && cache.schema !== TOWN_CACHE_SCHEMA) warnings.push(`schema '${cache.schema}' != '${TOWN_CACHE_SCHEMA}'`);
|
||||
if (cache.schema && !ACCEPTED_SCHEMAS.has(cache.schema)) warnings.push(`unknown schema '${cache.schema}' (want one of ${[...ACCEPTED_SCHEMAS].join(', ')})`);
|
||||
const c = cache.center;
|
||||
if (!c || !isNum(c.lat) || !isNum(c.lon)) errors.push('center.{lat,lon} must be finite numbers');
|
||||
if (!Array.isArray(cache.shops)) errors.push('shops must be an array');
|
||||
@ -87,6 +99,24 @@ export function validateTownCache(cache) {
|
||||
if (Math.max(latM, lonM) > MAX_TOWN_SPAN_M) warnings.push(`shops span ${(Math.max(latM, lonM) / 1000).toFixed(1)} km — likely more than one town (bound the query tighter)`);
|
||||
}
|
||||
}
|
||||
// v2: optional roads[] — simplified OSM ways. Each is `{ kind, pts:[[lat,lon],…] }` (id/name optional);
|
||||
// `kind` is the OSM highway=* class. Absent (or a v1 cache) ⇒ the marched-avenue fallback, so nothing
|
||||
// shipped regresses. A road needs ≥2 finite points; <2 is dropped, an unmapped kind falls to 'side'.
|
||||
if (cache.roads !== undefined) {
|
||||
if (!Array.isArray(cache.roads)) errors.push('roads must be an array (when present)');
|
||||
else {
|
||||
let badRoad = 0, shortRoad = 0, unknownKind = 0;
|
||||
for (const rd of cache.roads) {
|
||||
if (!rd || typeof rd.kind !== 'string' || !Array.isArray(rd.pts)) { badRoad++; continue; }
|
||||
if (rd.pts.length < 2) { shortRoad++; continue; }
|
||||
if (rd.pts.some(p => !Array.isArray(p) || !isNum(p[0]) || !isNum(p[1]))) badRoad++;
|
||||
if (!ROAD_KIND[rd.kind]) unknownKind++;
|
||||
}
|
||||
if (badRoad) errors.push(`${badRoad} malformed road(s) — need { kind:string, pts:[[lat,lon],…] }`);
|
||||
if (shortRoad) warnings.push(`${shortRoad} road(s) with < 2 points → dropped`);
|
||||
if (unknownKind) warnings.push(`${unknownKind} road(s) with an unmapped highway kind → 'side'`);
|
||||
}
|
||||
}
|
||||
if (!cache.license || !cache.attribution) warnings.push('missing license/attribution (ODbL required for shipped real caches)');
|
||||
return { ok: errors.length === 0, errors, warnings };
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user