sandy-locations · reference guide
← Review tool
01

Data pipeline

One state adapter (src/adapters/ca.ts) joins four sources into two output files. Only the government sources ever create or move a record — Google and human overrides only enrich or annotate one.

Government — authoritative External API — enrichment Output
CCC YourCoast — base fetch
the only step that creates records
toRecord + slug-uniquing
drops blank names, bad coords, non-beach facilities — Dropped bucket
+ CCC ArcGIS
accessType and amenity flags, by name-slug + proximity
+ CDPR dog policy
rule-owning agency for state park units — can demote or upgrade
+ human overrides final say
+ Google Places 30-day TTL, publish-status candidates only
enrichment only — never identity, never policy
data/out/ca.json
full inventory · never published
data/publish/ca.json
dog-positive subset · sandy app
CCC stays authoritative for identity throughout: no join step ever overwrites a record's name, lat, or lng.
02

Google matching

Runs only on the published-status candidates from step 1 — never the full inventory. Every number below is read live from GOOGLE_TUNING, not typed by hand, so retuning a threshold updates this page automatically.

Place ID cached?
yes → kept forever, no call
no → continue below
Unmatched, searched under 14 days ago?
yes → stays unmatched this run
no → text search → nearest candidate wins, not top relevance · costs a call
Distance / name gate
300m accepted, confident
800m accepted, flagged Google review
2000m only if name similarity ≥ 0.5
else stays unmatched
Details stale?
no → use cached details
yes → fetch hours, rating, contact · costs a call, max 1000/run

Showing built-in fallback values — could not reach /api/constants (is the review server running?).

03

Commands

Every script in package.json, and what each flag costs you. The default is deliberately free and offline — network access is something you opt into.

Command
Network
npm run seed -- --state=ca
Re-joins the four sources from data/cache/ and re-applies the committed Google cache. Writes both output files. Byte-identical on a re-run.
none
… --refresh
Re-fetches the three government sources instead of reading data/cache/. Still no Google calls.
gov
… --refresh-google
The only command that spends quota. Re-fetches Details for matched records past the 30-day window; hard-stops at MAX_DETAILS_CALLS. A missing GOOGLE_MAPS_API_KEY logs and continues.
quota
… --no-google
Government sources only. The committed cache is left on disk untouched, but no google block reaches the output.
none
npm run review
This tool, on http://localhost:4400. Local-only, zero dependencies, never deployed.
local
npm run typecheck
tsc --noEmit.
none
cd ../sandy && npm run pull-locations-v3
Copies data/publish/*.jsonsandy/data/master-v3.json. Restart the app's dev server after. Runs in the sibling repo, not this one.
none
The review tool's two buttons are these commands. Re-run seed spawns --state=ca; Refresh Google data appends --refresh-google and nothing else can add it.
04

Endpoints

Everything this project talks to, upstream and local. Adding a state adds rows to the first table only — the local API is state-agnostic.

Upstream — the pipeline's sources
GETapi.coastal.ca.gov/access/v1/locations
CCC YourCoast — the base fetch and the only source that creates records. Cached in data/cache/ (gitignored); re-hit only by --refresh. Free, unkeyed. Carries the licence and indemnity text every record must render.
GETservices9.arcgis.com/…/AccessPoints/FeatureServer/0/query
CCC ArcGIS layer — paged query, enrichment only. Unstable OBJECTIDs, so it never creates a record; matched by name-slug + proximity. Free, unkeyed.
GETwww.parks.ca.gov/Dogs
CDPR per-unit dog policies — an HTML table of 270+ units, parsed not APIed. The rule-owning agency: it can demote to prohibited or upgrade to leashed. Free, unkeyed.
POSTplaces.googleapis.com/v1/places:searchTextPro SKU · 5,000/mo free
Text search — runs once per record ever; the place ID is the one field the terms let us keep indefinitely. Headers X-Goog-Api-Key + X-Goog-FieldMask. Nearest candidate wins, not top relevance.
GETplaces.googleapis.com/v1/places/{placeId}Enterprise+Atmosphere · 1,000/mo free
Details — hours, rating, phone, website, accessibility flags. --refresh-google only, 30-day licence, capped at 1,000 calls per run. Cached in data/google-cache/, which is committed — that's why a plain seed needs no key.
Local — review server on :4400
GET/ · /referenceThe two pages, served from STATIC_FILES.
GET/api/dataEverything the tool renders in one payload: records, CDPR review buckets and drops, the Google report and cache, all three override files, publishStatuses, dogStatuses, googleTtlDays.
GET/api/constantsGOOGLE_TUNING + GOOGLE_TTL_DAYS, read live from the pipeline code — this is what keeps section 02 honest.
POST/api/override{ id, status, note } or { id, remove:true }data/overrides/ca.json. Unknown id throws.
POST/api/override/batch{ ids[], status, note } — the bulk bar. Replaces existing overrides after a confirm.
POST/api/google-override{ id, skip?, placeId?, textQuery?, trustCoordinates?, note }data/overrides/ca-google.json.
POST/api/google-override/batch{ ids[], skip:true, note } — bulk "skip Google matching".
POST/api/reviewed{ id, flagSignature, note? } or { id, remove:true }data/overrides/ca-reviewed.json. Backs "Match is right — clear flag"; a record falls back into Needs attention the moment its flag set changes.
POST/api/seed?state=caSpawns the seed and streams its log into the panel. &refreshGoogle=1 is the single place --refresh-google can enter the system — the only request here that can spend money.
05

Tuning constants

Read live from GOOGLE_TUNING in src/sources/googlePlaces.ts via /api/constants — the numbers here and in section 02 can never drift apart.

ConstantValueWhat it controls
DISTANCE_CONFIDENT_M300Auto-accept a match at or under this distance from the government coordinate.
DISTANCE_MAX_M800Accept on distance alone up to here; flagged for review above the confident threshold.
DISTANCE_EXTENDED_M2000Accept out to here when the name also agrees — recovers large parks/trails whose Google centroid sits far from the access point.
EXTENDED_NAME_SIMILARITY0.5Minimum name-token overlap required to accept a match beyond DISTANCE_MAX_M. Never a gate on its own — only ever widens the distance gate.
UNMATCHED_RETRY_DAYS14How long an unmatched record is left alone before it's searched again.
MAX_DETAILS_CALLS1000Hard cap on Details calls in a single run — the cost guardrail. A run stops rather than silently exceeding it.
GOOGLE_TTL_DAYS30Licence window. Enforced on every seed run, with or without an API key — an expired entry drops from the output even if it's still sitting in the cache.
06

Review-tool guide

Needs attention is the default view — the union of the five reasons in section 08, sorted worst-first. Re-run seed re-joins the government sources (using cached raw responses) and re-applies Google's already-cached data — zero external calls. Refresh Google data is the one action that spends quota; it's gated behind a confirmation that reports the live candidate count and free-tier headroom.

Filters compose: OR within a group (Pipeline or Google), AND across groups. They echo as removable pills below the controls, and survive a reload along with your search, sort, selection and scroll position.

Match is right — clear flag records a per-record review (data/overrides/ca-reviewed.json) keyed to the exact flags you saw. If those flags change on a later run, the record returns to Needs attention automatically.

Audit-only fields: a Google match's matchedName and its coordinate describe what Google thinks the place is called and where it is — shown so you can sanity-check the match. Neither ever overwrites the record's real name, lat, or lng; those stay the government's, always, unless a human explicitly sets trustCoordinates.

Bulk selection persists across filter and search changes — picking records in one view, then narrowing the list to check a couple more, won't lose your selection. The bulk bar tells you when some of your selection is currently hidden by the active filter.

07

Keyboard shortcuts

The review tool is a queue you walk, not a page you browse — everything below works from anywhere on the page except while you're typing in a field.

Your view, filters, search, sort, selection, and scroll position all survive a reload.

08

Chips and buckets

Needs attention is a computed queue, not a pipeline bucket of its own — every record matching at least one of five reasons a human should look at it, ranked worst-first. A record is never double-counted for matching more than one reason.

ReasonWhat it means
ChipMeaning
Adding a state — one adapter in src/adapters/, one registry line in adapters/index.ts, and widen SourceId + LICENSES in types.ts. seed.ts needs no changes — and this guide needs only new rows in the upstream table above.