Live: https://bitemap-workflo17.vercel.app
Every restaurant in New York City, mapped by cuisine — all 30,075 licensed
restaurants in the five boroughs on one 3D map, with curated food crawls
rendered as subway strip maps. Queens loads first; the other boroughs stream
in behind it. (Formerly "Nom York"; internal ids — the repo folder, the
nomyork preview name, and localStorage keys — keep the old slug so nothing
breaks.)
Live data: NYC DOHMH restaurant inspections (open data, no key needed). Stack: vanilla JS + MapLibre GL, zero build step, zero paid APIs.
node server.mjs # serves web/ on http://localhost:4730
Or from Claude Code: preview nomyork (registered in ~/.claude/launch.json).
State round-trips through the URL:
?g=himalayan,thai— preselect cuisine groups?tag=sichuan— preselect name-pass sub-cuisine tags?crawl=momo-crawl— open a curated crawl with its route drawn?spot=40926286— open a restaurant detail card (CAMIS id)?q=momo— run a search (dish words expand to tags: momo, birria, plov…)?my=camis,camis,…— load a shared custom crawl into the builder?near=1— enable the Near me filter on load (triggers geolocation)?fake=40.7472,-73.8914— mock GPS position (demos + headless QA)&shot=1— static camera (no intro animation), used by the QA harness
Crawls preview as a user-paced stepper ("Preview stops"): Back / Next stop buttons or ← → keys, Esc ends, and the map stays free to pan and zoom between steps.
Search supports / to focus, arrow keys through results, Enter to pin all
matches on the map. The My Crawl builder (Crawls tab) reorders stops by drag
(grip handle; ↑/↓ buttons remain the keyboard/touch path), optimizes with
nearest-neighbor "Sort by walk", persists to localStorage, and copies a
shareable ?my= link.
Walk-mode routes (custom crawls + walking curated crawls) are routed along real streets via the FOSSGIS OSRM foot profile (routing.openstreetmap.de), with exact mileage replacing the straight-line estimate when the response lands; straight dashes remain as instant feedback and as the fallback when the router is unreachable. Subway-mode crawls (International Express) keep the straight dashed line by design — those hops are train rides.
The map control (bottom right) is MapLibre's geolocate control with
follow-me tracking: blue dot + accuracy ring. With a fix, distances appear
in detail cards ("0.3 mi away"), search results, the crawl HUD ("0.4 mi to
stop 2"), and "Near me" filters the map to spots within 0.75 mi and re-sorts
editor picks by proximity. Geolocation only works over HTTPS or on
localhost — a phone hitting the LAN IP over http gets a clear error hint
instead. For a phone demo, either tunnel with HTTPS or use ?fake=lat,lng.
Under 900px the rail becomes a collapsible bottom sheet over a full-screen map (drag-handle toggles, search focus auto-expands, safe-area insets respected, 28px touch targets for builder controls).
Anonymous comments on spots ("Tips from eaters" in detail cards) and crawls
("Crew notes" on crawl cards): optional name, 500-char tips, delete-your-own
via a local anonymous token. Served by a tiny API inside server.mjs
(GET/POST/DELETE /api/comments, JSON persistence in data/comments.json,
validation + per-IP rate limiting, HTML-escaped rendering). On static hosts
with no API the feature hides itself — moving comments to a deployed backend
(Vercel KV or similar) is the follow-up when the site goes public.
Beyond name regexes and the OSM join, data/enrich/llm_tags.json is a
reviewed classification overlay (per-CAMIS tags with confidence + source:
llm-name or menu-web) merged on every pipeline run, plus chain-level
rules in the pipeline (Golden Krust → Jamaican, etc.). The pipeline also
emits candidate lists (data/enrich/cand_*.txt) of spots still stuck in
coarse buckets — the work queue for future classification batches
(remaining: ~880 Latin American, ~2,090 Chinese). Precision-first: only
confident calls get tags; everything else stays in its parent bucket.
- Borough chips (five boroughs, counts), citywide cuisine + sub-cuisine chips.
- Open now (beta): hours joined from OpenStreetMap (~4,400 spots), evaluated by a parser for common patterns; unknown-hours spots are excluded when on.
- Late night (beta): hours that cross midnight or run 24/7 (~790 spots).
- Cheap eats (beta): OSM fast-food tagging + inherently cheap categories + $-priced curated picks (~5,400 spots).
- Profile tab ("You"): display name (auto-fills community tips), saved places, been-here check-ins, hearted curated crawls, named saved crawls, and Export/Import (JSON) so the no-account model can move between devices.
- Save / Been here flags per spot (localStorage) + "Saved spots only" filter.
- Named saved crawls (Save as… in the builder) with load/delete, listed in the profile.
- Walk mode: with a GPS fix the stepper shows distance to the current stop and an "Arrived ✓" button that checks stops off as you go.
- About & data credits page (about.html); OG share card (og.png, generated from docs/og.html).
vercel.json is configured (static from web/, clean URLs, cached data).
From an authenticated machine:
npx vercel login
npx vercel --prod --yes
# 1. fetch raw rows (corridor zips) + citywide chain table — see docs below
# 2. transform:
node pipeline/build_data.mjs # -> web/data/restaurants.json + stats
Raw fetches use curl.exe --ssl-no-revoke (Socrata API; revocation checks are
blocked on this network). Citywide: all inspection rows paged 50k at a time
into data/raw/city_p*.json, plus an Overpass pull of NYC food POIs
(data/raw/osm.json) for the OSM join (granular cuisine tags + opening
hours, matched by normalized name within 160 m — 7,242 joins). The tag
taxonomy covers ~50 regional sub-cuisines, grouped by region in the rail
(Regional China, Southeast Asia, Latin America, Caribbean, Africa…):
Jamaican 144, Vietnamese 137, Colombian 85, Sichuan 59, Peruvian 40, Cuban
28, Salvadoran 19, Yemeni 17, South Indian 16. Output is one
minified file per borough (web/data/spots-*.json). The pipeline dedupes
inspection rows by CAMIS, extracts
the latest letter grade, flags chains (5+ locations citywide after name
normalization, with a cuisine-carrying allowlist: Xi'an Famous Foods, Kennedy
Fried Chicken, Paris Baguette, Tous Les Jours, Jollibee, Halal Guys), tags
sub-cuisines from word-boundary name regexes (Sichuan, Nepali/Tibetan,
Colombian, ...), and marks not-yet-inspected spots as "just opened".
Hand-authored layers (join by CAMIS at runtime):
web/data/curated.json— 31 editor picks with seed scores, blurbs, anddishes(what-to-order recommendations shown on detail cards, in crawl previews, and indexed by search — "clothesline" finds Szechuan Mountain House). Scores and dish picks are prototype placeholders, not user data.web/data/crawls.json— 4 curated crawls (International Express, Momo Crawl, Elmhurst Thai Sprint, Flushing Dumpling Run).
The map is WebGL; in-app browser screenshots can silently capture nothing when the pane isn't compositing. Use the CDP harness (real-time waits, real GPU):
node tools/shot.mjs "out.png|http://localhost:4730/?shot=1&crawl=momo-crawl"
Reference captures live in docs/shots/.
- Basemap/tiles from OpenFreeMap public instance; style falls back to a flat canvas if unreachable.
- Editor scores/rankings are seeds; the ranking system decision (editorial vs community vs composite) is still open.
- Sub-cuisine tags cover distinctive names only (~name-pass floor); the Foursquare-OSM join + LLM pass from the enrichment plan is not built yet.
- Walking distances start as haversine × 1.3 estimates and upgrade to routed OSRM mileage when the public router responds; heavy traffic should move to a self-hosted OSRM/Valhalla before launch (public instance is courtesy-use).
- HTML5 drag-and-drop doesn't fire on touch screens; ↑/↓ buttons are the touch path.