4bd8481be1
The owner asked twice why there are still three separate boards. The honest answer, and what this round executes: **it feels like three boards, but not because the scale jumps 20x — because the three packs draw three different Californias, and the loudest difference is that the mountains are four times taller on one of them.** **THE 20x HORIZONTAL SCALE JUMP IS INVISIBLE**, and measuring that collapsed the cost of this whole round. `World.project` is a uniform scale in x/z with no vertical term, and a uniform scale leaves a perspective image identical — so a camera carried across the seam on matched true-metre offsets draws a pixel-identical horizontal frame. 1,919 -> 94 m/unit costs nothing to look at. Rescaling was never the problem. A boot card, a tab strip and a 4.17x vertical deflation were. **THE PAUSE WAS MOSTLY FAKE.** A switch covered the screen for 1,715 ms but only 608 ms blocked the main thread; the page drew 46 of 69 frames with nothing to show, because the outgoing board had already been disposed. `mountCity` now retains it: the incoming board builds BEHIND a live, interactive picture, and `stage.setScene` fires only on completion. Measured across all six directions, three runs each — boot card yes -> **no**, opaque cover 726-1,415 ms -> **0**, blank frames 21-46 -> **exactly 1**, wall clock down 12-29%, blocked main thread down 15-47%. A return to a board already seen links **zero** shader programs and blocks **zero** milliseconds: 298-312 ms of camera flight where it was ~1,600 ms behind a card. Disposal had been throwing away the shader cache too — linkProgram ran 38, 59, 78, 109, 127 across five mounts and never reused one. **The transition is a fog dip, not a crossfade**, through the `setAerialFog` seam built last round. Every both-boards-live crossfade breaks a budget — ca+sf is 2,640,307 triangles against bay-area's 2,600,000 cap — and a fade never lands inside the harness's sample window, which is the "a cap you do not measure is a cap you do not have" failure this repo already argues against. The dip costs zero triangles and zero draw calls, and it hides the 4.17x deflation, the 4,025 m projection disagreement and the vanishing 2 km freeway symbols at once, because all three happen at maximum obscuration. It is also diegetic: a descent through haze. The first dip was wrong and the photograph caught it: collapsing to 6% of board SPAN turned the whole night frame into one flat field — the exact "turns the map off" failure the risk list named. Re-anchored to 70% of camera STAND-OFF, so the coastline survives and only the relief melts. **One ladder, one places list.** 26 authored chapters become 24 rungs sorted descending by STAND-OFF, not altitude — by altitude they interleave badly and altitude cannot tell a low oblique from a high plan. The three-board tab strip is off by default; the left column is now one scrolling list of all 24 rungs under three region headings that does not change when the board does. Only which row is lit changes. Label collisions are resolved in the ladder and never in a pack, so the 29 index-aimed capture guards are untouched. The minimap stops turning through 90 degrees between boards: every board is pinned to a rectangle with California's proportions. **SF and SoCal are not regressed**, and that was the acceptance that mattered: 95.9-98.8% of board pixels are delta-0 against a baseline hash-verified identical to what the live site serves, and every one of the 34-70 surviving pixels per frame is an aircraft or a hull. **A real defect found only by photograph:** `minimap.setMarkers()` had zero call sites. Every marker on every board was gone — the LA studio's door dot, the Bay Area's eight company markers — dropped when the minimap went per-board. Typecheck, tests, budgets and the console were all green with that bug in. Also fixed: two capture presets that lied. `look.mjs`'s `glyph-la` and `glyph-sf` claimed California chapter closeups and returned SoCal and Bay Area frames, because they aimed by chapter index and the indices had moved. Aiming is now by identity, with a guard test. NOT SHIPPED, DELIBERATELY: the pack merge. At Bay density it is 34.04M triangles, 13x the highest budget — dead, not a trade. At SoCal density it is 1.99M and fits today, and the price is San Francisco rendering at 164 m lots instead of 40 m, i.e. SF looking the way SoCal looks now. SF and SoCal carry every marketing still on the site. That is the owner's decision and it is worthless as an argument and decisive as a photograph, so it ships as a measurement artifact with a side-by-side still and is wired into nothing. The four data reconciliations that would make one world honest — one exaggeration rule, roads in metres, one projection centre, one coastline convention — are behind TERA_RECONCILE, default OFF. Tests 1,570 -> 1,651, server 295. All ten budget cells pass, no cap raised. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
374 lines
20 KiB
Markdown
374 lines
20 KiB
Markdown
# The contract
|
||
|
||
Five designs were produced in parallel and three critics tore into them. They
|
||
collided on fifteen blocking points — four files were specified twice with
|
||
incompatible contents, three separate backends were designed for one box, and
|
||
one type name was exported twice meaning different things.
|
||
|
||
This file is the resolution. **It wins over any individual design.** Anything
|
||
implemented here must match this document; where a design said otherwise, this
|
||
document is why it changed.
|
||
|
||
---
|
||
|
||
## 0. Scope
|
||
|
||
We are building **Lumbridge's own** Tera and office, plus a **dev kit** so
|
||
anyone can run their own on their own hardware.
|
||
|
||
Not in scope, deliberately: renting or purchasing parcels, billing, per-tenant
|
||
provisioning, cluster placement, a membership/tenancy system. Federation — a
|
||
self-hosted office announcing itself to a shared Tera — is a maybe-later that
|
||
the adapter boundary leaves room for and nothing designs toward now.
|
||
|
||
**The acceptance test for every decision below**, and the one that failed
|
||
end-to-end in the design round:
|
||
|
||
> A stranger clones the repo, runs one command, gets a city, copies the
|
||
> reference office pack, and has their own office — with no Lumbridge account,
|
||
> no Supabase project, and no keys.
|
||
|
||
Two CI jobs make that testable rather than aspirational, and they gate the repo:
|
||
`git clone && npm ci && npm run build`, and `docker compose up` under `env -i`
|
||
asserting `GET /api/v1/health` → 200.
|
||
|
||
---
|
||
|
||
## 1. Stage and scene lifecycle
|
||
|
||
Two designs both created `src/engine/stage.ts` with opposite lifecycles. The
|
||
retention argument wins on measured cost: the Bay Area's heightfield is 0.53M
|
||
lattice points and about 2.3 s to build — it was 336,864 and ~1.0 s when the
|
||
pack was San Francisco alone, and it grew, which is the point. Disposing the city
|
||
every time someone steps into an office and paying a second of rebuild on the
|
||
way out is not acceptable.
|
||
|
||
- **`Stage`** (`src/engine/stage.ts`) owns *only* the renderer, the RAF loop,
|
||
resize, and a swappable current scene. Nothing else.
|
||
- **`StageScene`** carries its own `THREE.Scene`, `PerspectiveCamera` and
|
||
`OrbitControls`. `stage.setScene(s)` swaps; the outgoing scene is **retained
|
||
and paused, never disposed**.
|
||
|
||
### 1.1 A city board is retained across a **board** switch, not only an office visit
|
||
|
||
This clause used to be about one door: the city was kept because you were coming
|
||
straight back out of an office. It now covers the other direction too, and for
|
||
the same measured reason.
|
||
|
||
A board switch used to dispose the outgoing board at the top of `mountCity` and
|
||
then build the next one behind a full-screen card. Measured against `bcac6aa`
|
||
with a real GPU, that card covered the screen for 1,711 / 1,079 / 997 ms
|
||
(bay-area / socal / california) while only 604 / 372 / 222 ms actually blocked
|
||
the main thread, and the page drew 68 / 45 / 49 frames inside the gap — so
|
||
two thirds of every pause was a live, animating page with nothing to draw.
|
||
Disposal was also throwing away the shader cache: `three` refcounts programs per
|
||
material and deletes at zero, `linkProgram` ran 38 → 59 → 78 → 109 → 127 times
|
||
across five mounts and never once reused, and a second visit to the Bay Area was
|
||
only 115 ms faster than the first.
|
||
|
||
So:
|
||
|
||
- **`main.ts` holds a `Map<boardId, MountedBoard>`** (`src/engine/boards.ts`),
|
||
and the incoming board is built **behind** the outgoing one. The outgoing board
|
||
stays `stage.current()`, live and interactive, for the whole build.
|
||
`stage.setScene` is called only on completion, under a fog dip.
|
||
- **`createScene` no longer presents what it constructs.** `SceneOptions.present`
|
||
defaults to `true` so every existing caller is unchanged; `present: false` says
|
||
"build it, do not show me yet", and the caller that asks owns the
|
||
`stage.setScene(handle.stageScene)`.
|
||
- **Who disposes a board.** Still whoever built it, which is now the cache's
|
||
eviction: `boards.put()` returns whatever it dropped and `main.ts` disposes
|
||
exactly that. `SceneHandle.dispose()` is unchanged and still calls
|
||
`environmentRig.release(scene)` first — the rig's ledger is a strong reference,
|
||
and a disposed board left in it retains the whole scene graph.
|
||
- **What is retained and what is not.** A `MountedBoard` holds everything
|
||
expensive or derived from the pack (the handle, the plan view, the atmosphere,
|
||
the board span, the harbour model, the traffic dial). It does **not** hold
|
||
subscriptions — the weather watch, the fire watch, the fire panel, the camera's
|
||
fog listener and the pose editor belong to the board that is *on screen*, and
|
||
are stopped and restarted as boards swap.
|
||
- **Residency.** California is pinned (5.86 MB of GPU buffers, and the root of
|
||
every descent). Three boards resident on a desktop, two on a handheld —
|
||
`deviceProfile()` changes only the pixel ratio and the shadow map, so a phone
|
||
carries the same resident geometry as a laptop against a much smaller budget.
|
||
Two is provably enough: `sf` and `socal` bounds do not intersect and California
|
||
contains both, so the set of boards that can describe one camera position is
|
||
always `{california}` or `{california, one metro}`.
|
||
- **The boot card is the first mount of a session and nothing else.** Every
|
||
switch after it raises a small non-modal pill instead. Measured after this
|
||
change, all six board-to-board directions cover **zero** frames with the boot
|
||
card, and a return to an already-visited board links **zero** shader programs
|
||
and blocks the main thread for **0 ms**.
|
||
- Camera, controls, lights and picking live in a per-scene **`SceneKit`**
|
||
helper that both `createScene` (city) and `createOfficeScene` construct.
|
||
They do *not* live on Stage.
|
||
|
||
Rationale for the split: the city and an office cannot share a `THREE.Scene` at
|
||
all. SF's `latScale` puts one scene unit at ~94 m with 3.6× vertical
|
||
exaggeration; an office renders at 1 unit = 1 m. Two scenes, one renderer.
|
||
|
||
```ts
|
||
export interface StageScene {
|
||
scene: THREE.Scene
|
||
camera: THREE.PerspectiveCamera
|
||
controls: OrbitControls
|
||
onEnter?(): void
|
||
onExit?(): void
|
||
tick(dt: number, elapsed: number): void
|
||
dispose(): void
|
||
}
|
||
export interface Stage {
|
||
renderer: THREE.WebGLRenderer
|
||
setScene(s: StageScene): void
|
||
current(): StageScene | null
|
||
dispose(): void
|
||
}
|
||
```
|
||
|
||
## 2. The office data contract — one file, one type
|
||
|
||
`src/interiors/plan.ts` was specified twice with incompatible contents, and a
|
||
fourth design imported an `OfficeDoc` no design produced.
|
||
|
||
- **`src/interiors/types.ts` is the single authored contract.** It exports
|
||
`Office`, and must stay JSON-serialisable — no functions, no classes, no
|
||
THREE types — because a hand-written pack and one arriving over HTTP have to
|
||
be the same thing.
|
||
- Walls are an **explicit segment list with 1-D `openings`**. Room polygons
|
||
imply no walls; they are floor slabs. This wins because the opening-splitting
|
||
pass produces the walk-mode collision segments for free, and because
|
||
auto-generating walls from shared room edges needs float-equality dedup.
|
||
- **Doors and windows are `Opening` records punched out of a wall**, not
|
||
placeable assets. `shell.wall.ts`, `shell.door.ts` and `shell.window.ts` are
|
||
deleted from the asset library; `shell.ts` calls a parameterised `wallRun`
|
||
part per solid run. Shipping both would put every opening in the scene twice,
|
||
or leave the collider with no gaps.
|
||
- **`src/interiors/plan.ts` is the `Plan` class** — the `World` analogue.
|
||
Resolves an `Office` once into wall runs, seats, props, collision segments and
|
||
bounds. The other design's `Placement[]` is **`Plan`'s output**, an internal
|
||
build input, not a second authored format.
|
||
- `OfficeDoc = { id, name, floor: Office, … }` lives in `src/server/wire.ts`,
|
||
not in interiors.
|
||
|
||
## 3. Assets
|
||
|
||
- **Procedural TypeScript only.** Every mesh is a function composing cached unit
|
||
primitives; every texture is drawn on a 2D canvas from seeded noise. No binary
|
||
art is ever committed. This is the same property that gives the city zero
|
||
asset-licensing exposure, and it is worth more than the quality ceiling it
|
||
costs.
|
||
- The **assets registry wins** over the interiors one — it is strictly richer
|
||
(roles, quality, overrides, footprint-without-build). `src/assets/kit.ts`
|
||
defines `AssetDef` and `AssetId`; `src/assets/materials.ts` defines
|
||
`MaterialRegistry` keyed on a closed `SurfaceRole` union.
|
||
- **`Prop.kind` is an `AssetId`.** The prop registry and the asset registry are
|
||
the same registry.
|
||
- Namespace is **`tera:`** (`tera:desk.workstation`), not `lse:`. A self-hoster
|
||
registers `acme:desk.standing` with `overrides: "tera:desk.workstation"` and
|
||
reskins without forking.
|
||
- Assets are authored in metres, 1 unit = 1 m.
|
||
- `ghostOf()` moves onto the assets `MaterialRegistry` for the wall-occlusion
|
||
fade.
|
||
|
||
### 3.1 The art licence lands with the first asset file
|
||
|
||
Apache 2.0 covers the code. The **artistic output is additionally dedicated
|
||
under CC0-1.0**, so a mesh can leave this repo without dragging a NOTICE
|
||
obligation into someone else's project.
|
||
|
||
This is decided now, not later, and `src/assets/LICENSE-ART` plus the
|
||
`CONTRIBUTING.md` inbound terms land **in the same commit as the first file
|
||
under `src/assets/`**. Deferring it is the expensive mistake: eighteen asset
|
||
builders and a ~180-prop reference office is a lot of authored work to
|
||
accumulate before anyone states the terms, and relicensing art once
|
||
contributors exist is close to impossible.
|
||
|
||
The inbound grant must be **standing, not per-PR**. Apache 2.0 §5 supplies a
|
||
default inbound=outbound grant for Apache-2.0 only; there is no default inbound
|
||
CC0, so a single merged PR whose author never said the words leaves that
|
||
contribution Apache-only and makes a directory-wide claim false. `CONTRIBUTING.md`
|
||
therefore carries a DCO-style sentence — *by submitting a change under
|
||
`src/assets/`, you license it under Apache-2.0 and dedicate the artistic output
|
||
under CC0-1.0* — so submission itself is the grant. `LICENSE-ART` is worded as a
|
||
dedication made by the copyright holders of the material, not as a property of
|
||
the directory. NOTICE gains the carve-out, because NOTICE is what a downstream
|
||
consumer actually reads to learn the repo is not uniformly Apache-2.0.
|
||
|
||
## 4. Lighting and environment — one owner, one direction
|
||
|
||
`Environment` was exported twice meaning different things, and two modules both
|
||
constructed and mutated the same three lights.
|
||
|
||
- **`Environment` is an observation**: `{ time, sun: SolarPosition, weather }`.
|
||
- The lighting *state* is renamed **`LightingState`**.
|
||
- **`Atmosphere` is the sole light owner.** `atmosphere.apply(env) →
|
||
LightingState`, which the scene applies. One direction, no write-backs.
|
||
- Solar position is computed locally with **no network** — a NOAA/Meeus
|
||
implementation in `src/engine/solar.ts`, dependency-free.
|
||
- An **office with no `site` gets no Atmosphere**: `fog: null`, no
|
||
`scene.background` drive, interior lighting is its own fixed rig. This is
|
||
still the default and still the promise — a pack can be authored, rendered and
|
||
shared without owning a coordinate, an account or a network.
|
||
- **An office that declares a `site` gets the same sun the city does.** This is
|
||
the "later refinement" this clause reserved, taken up rather than a reversal of
|
||
it: `Atmosphere` is still the sole light owner and there is still one direction
|
||
of flow. `interiors/daylight.ts` adapts what `apply()` returned; it computes no
|
||
light of its own.
|
||
|
||
Two things are true only indoors, and they are the whole of the adapter:
|
||
|
||
- **The building is rotated.** `Atmosphere` works in the city's frame, where
|
||
−Z is north because a city pack is a map. `OfficeSite.heading` is the bearing
|
||
the pack's −Z actually points along, and the sun is turned by it — otherwise
|
||
"the daylight side" in a pack's comments is a label rather than a fact.
|
||
- **The fog starts outside.** A city fog beginning 1,150 units away is fine at
|
||
94 m per unit and is *inside the room* at 1 m per unit. The colour is kept
|
||
and the distances are replaced.
|
||
|
||
A sited office also gets a `sky` and a ground plane at `-site.elevation`, which
|
||
is what makes 188 m up a tower feel different from 4 m above an airfield.
|
||
|
||
**A third input joined the adapter, and it is still not a second opinion about
|
||
the light.** `officeDaylight(state, site, smokeLoad)` takes a 0–1 scalar for
|
||
how much wildfire smoke is in this building's air, and moves the haze colour,
|
||
the haze near-distance and the sun's tint — three numbers the adapter already
|
||
computed. It is clamped at the boundary, bit-identical to today at zero, and it
|
||
changes nothing about who owns the rig: `Atmosphere` still decides the sun and
|
||
the sky, `daylight.ts` still only adapts what `apply()` returned, and the
|
||
scalar is handed *in* from `main.ts` rather than fetched. A room may not reach
|
||
into the fire layer and decide its own sky; that would be the second sun this
|
||
clause exists to prevent, arriving through a side door.
|
||
|
||
The fire layer itself constructs no light of any kind. A burning hillside at
|
||
night is emissive material plus one additive ground quad in the same instanced
|
||
mesh — the mechanism `nightlights.ts` uses for San Francisco's 12,038 street
|
||
lamps — because a `PointLight` per fire is exactly the case this clause forbids
|
||
and exactly the case that tempts one.
|
||
|
||
## 5. One server
|
||
|
||
Three backends were designed for one box — three ports, three frameworks, three
|
||
`deploy/Caddyfile.snippet` files that would overwrite each other.
|
||
|
||
- **One Fastify workspace**, `server/`, listening on `127.0.0.1:8431`, serving
|
||
`/api/v1/*`. One systemd unit, one Caddy snippet.
|
||
- Weather and office routes fold in as route modules, not services.
|
||
- Env prefix is **`TERA_*`** throughout.
|
||
- The Workie sync oneshot stays the **only** second process — it is the sole
|
||
holder of a Workie credential, and that isolation earns itself.
|
||
- Private per-user markers are **never proxied**. The authenticated browser
|
||
calls Workie directly with its own token, so private rows never transit the
|
||
public box.
|
||
- `Cache-Control` is fail-closed: a global hook stamps `private, no-store`, and
|
||
a route opts in to public caching explicitly.
|
||
- Authenticated realtime is a route module in this same process, not another
|
||
daemon. Its state is bounded and memory-only. Event credentials travel in a
|
||
POST body, never a URL; the server owns interest filtering, motion validation,
|
||
token rotation and revocation. The auth subject is not a peer-visible entity
|
||
id, and no media is carried on the game-state stream.
|
||
|
||
### 5.1 Zero-config boot must actually boot
|
||
|
||
Both server designs made the same independent mistake: a weather source
|
||
defaulting to a provider that requires a contact string, with a hard failure
|
||
when it is absent — which fails the very acceptance test they named.
|
||
|
||
- `TERA_WEATHER_SOURCE` defaults to **`none`**.
|
||
- A source set without a contact is a **demotion, not a fatality**: log one loud
|
||
line and serve the `synthetic: true` clear-day body.
|
||
- The `env -i` CI job is what keeps this honest.
|
||
- **`TERA_FIRES_SOURCE` defaults to `none`, and `none` invents nothing.** The
|
||
asymmetry with the weather is deliberate and is the sharpest line this round
|
||
drew: an invented clear day is a defensible synthetic default, and an invented
|
||
wildfire is a claim that a named place is burning, made to somebody who may
|
||
live there. So `none` serves a real, empty body, and the board says how old its
|
||
last answer is rather than showing an all-clear it cannot support.
|
||
- Off-by-default is a **choice**, not a misconfiguration, so an unset
|
||
`TERA_FIRES_SOURCE` appends nothing to `degraded[]` — which is one sentence per
|
||
*demotion*. `scripts/check-zero-config-boot.mjs` refuses to pass with any
|
||
demotion at all on an empty environment, and that is the gate that keeps the
|
||
distinction real: a stranger's clone is not a broken deployment.
|
||
|
||
### 5.2 Weather sources
|
||
|
||
`api.weather.gov` (NWS) is US-government public domain, keyless, and the default
|
||
*once a contact is configured*. `met.no` is the global fallback. Open-Meteo is
|
||
opt-in and off by default: its data is CC-BY 4.0 but its free tier is
|
||
non-commercial, which is the wrong default for a product page.
|
||
|
||
## 6. Auth
|
||
|
||
Scope-corrected: no membership tables, no tenancy.
|
||
|
||
- **`TERA_AUTH_MODE` defaults to `none`.** A self-hoster gets an open office and
|
||
never creates an account anywhere.
|
||
- Lumbridge's own office uses `sso` mode, reusing the pattern already running on
|
||
the fleet: the world holds **no credentials**, is handed an entry URL and a
|
||
**server-side revalidate URL**, and enforcement happens on the server. Both
|
||
are env vars, which is exactly what a dev kit needs.
|
||
- Where a JWT is verified directly, **HS256 against a shared secret is primary**;
|
||
JWKS sits behind an env switch. This is a correction from verified fact — the
|
||
fleet's Supabase issues `{"alg":"HS256"}`, so a JWKS-only implementation would
|
||
reject every real token.
|
||
- A private office returns **404, not 403**, so the endpoint cannot be used to
|
||
enumerate what exists.
|
||
- **A refused feed must produce a working instrument, not a dead one.** The
|
||
studio device route is members-only and answers 401 to an anonymous GET, which
|
||
is correct — the microphones and the camera behind it are hardware in
|
||
somebody's room. What is *not* correct is asking anyway. `Capabilities` now
|
||
carries `liveDevices` (`tier !== "anon"`) alongside `Feeds.devices`, and both
|
||
must be true before the API strategy is chosen; when either is false the
|
||
bundled fixed-step simulator runs in the tab instead. Passing only the
|
||
deployment's half is what shipped a permanently at-rest instrument panel to
|
||
every anonymous visitor on cloud-2, backing off exponentially against a
|
||
request that could never pass. This is the same anon-first rule
|
||
`SimulatedFlights` and `sample.ts` already follow, applied to the one feed
|
||
that had a real refusal behind it.
|
||
- `@supabase/supabase-js` is a **real dependency**, and the "no surprise
|
||
dependencies" CI check becomes an **allowlist naming why each is permitted**,
|
||
not a count. A dynamic import of an uninstalled package fails the Vite build,
|
||
which would have made the `auth: none` default — the committed default —
|
||
unbuildable.
|
||
|
||
## 7. The binary gate must not fire on the self-hoster
|
||
|
||
The no-binary-art check is a promise about **this repo's committed art**, but as
|
||
designed it walked the working tree and would fail a self-hoster's build on
|
||
their own legally-clean `.glb` — while two other designs told them to put files
|
||
exactly there.
|
||
|
||
- Enumerate via **`git ls-files`**, never a filesystem walk, so untracked local
|
||
assets are invisible to it.
|
||
- Strict over **`src/**`** — that is where the licensing argument lives.
|
||
- Hard-exempt `public/props/`, `public/kits/`, `public/offices/`, `docs/`, and
|
||
add them to `.gitignore` marked as self-hoster space.
|
||
|
||
## 8. Geocoding — the correction that matters most
|
||
|
||
`ARCHITECTURE.md` §3.2 argued that keeping geocoded company coordinates out of
|
||
the repo solved the ODbL problem. **That reasoning is wrong**, and this is the
|
||
sharpest thing the critics found.
|
||
|
||
Containment solves licence *mixing inside the repo*. It does not touch ODbL's
|
||
actual trigger. Serving a snapshot of OSM-derived coordinates at a public
|
||
endpoint is **Publicly Using a Derivative Database**, which brings ODbL §4.3
|
||
attribution and §4.4 share-alike onto the served data regardless of where the
|
||
rows are stored. Storing them off-repo hides the obligation; it does not
|
||
discharge it.
|
||
|
||
Nothing is committed to yet — Workie has no geocoding code today — so:
|
||
|
||
- The geocoder is the **US Census Geocoder** (`geocoding.geo.census.gov`): a US
|
||
Government work in the public domain, keyless, and covering SF, LA and NYC,
|
||
which is every city planned.
|
||
- **Google, Mapbox and HERE do not solve this either** — their terms restrict
|
||
storing and redistributing returned coordinates, which is precisely what a
|
||
public snapshot does.
|
||
- The sync script records a **per-row provenance field**, and the public-shape
|
||
assertion **rejects any row whose provenance is not on a non-ODbL allowlist**.
|
||
The gate that already checks field names now also checks where a coordinate
|
||
came from.
|
||
- `NOTICE`'s GEOGRAPHIC DATA block gains this next to the existing USGS/SRTM
|
||
sentence, and `ARCHITECTURE.md` §3.2's reasoning is corrected rather than
|
||
quietly left standing.
|