OpenData Vibe Assessment: Ronaldi Saphala

Address passport. Type an address, geocode to BBL/BIN via PLUTO or Geoclient, then fan out to fifteen datasets in parallel (HPD violations, DOB permits and complaints, 311, ECB, restaurant inspections, evictions, ACRIS if you scrape it) and render one page: everything the city knows about this building. The portal can't do this because it has no concept of a cross dataset key, but BBL and BIN are shared across dozens of tables.

Layered map with hex aggregation. MapLibre plus H3 in the browser. Stack 311 noise complaints over liquor licenses over DOB after hours permits, aggregate to hexes on the fly, and slide a date range. Socrata's map viewer does one layer and chokes on anything big.

Natural language to SoQL. Pull column metadata from /api/views/{id}.json, hand it to an LLM, generate the SoQL, run it, and show both the answer and the query so people learn the syntax. 

Prompt:

You are building "NYC Data Lens", a single page web app on top of NYC Open Data
(data.cityofnewyork.us, Socrata / SODA API). The portal's UI is weak but the API
is strong: SoQL queries, CORS enabled, JSON and GeoJSON output, no API key needed
for moderate volume. Treat the portal as a read only database and do all joins,
geometry, and time aggregation in the browser. There is no backend of our own.

STACK AND CONSTRAINTS
- Vite + TypeScript + React. Tailwind for styling. MapLibre GL for maps.
  h3-js for hex aggregation. Zustand or plain React state; keep it simple.
- All state that defines a view (dataset ID, SoQL, map viewport, date range,
  active layers, current address) must be encoded in the URL so every screen
  is a permalink. Loading a URL must fully reconstruct the view.
- Every SODA call goes through one client module: buildUrl(datasetId, soql),
  fetchJson, fetchGeoJson. Use $query for full SoQL, $limit paging, and
  $$app_token from an env var if present. Cache responses in memory keyed by
  URL. Show request errors inline with the failing query visible.
- Keep a datasets.ts registry with, for each dataset: id, human name, primary
  date column, location columns (lat/lng or the_geom), and join keys it
  carries (bbl, bin, community_board, council_district, zip, nta). Seed it
  with these and VERIFY each ID against the catalog before relying on it:
    311 Service Requests            erm2-nwe9
    PLUTO                           64uk-42ks
    HPD Violations                  wvxf-dwi5
    DOB Permit Issuance             ipu4-2q9a
    DOB Complaints                  eabe-havv
    ECB Violations                  6bgk-3dad
    Restaurant Inspections          43nn-pn8s
    Motor Vehicle Collisions        h9gi-nx95
    Evictions                       6z8x-wfk4
  Column names for each must be pulled live from
  https://data.cityofnewyork.us/api/views/{id}.json, not hardcoded, because
  schemas drift.
- Geocoding: use NYC Planning Labs Geosearch
  (https://geosearch.planninglabs.nyc/v2/search?text=...). It returns BBL,
  BIN, borough, and coordinates with no API key.
- No dashes in UI copy. Plain, direct language.

FEATURE 1: ADDRESS PASSPORT
Route: /building?address=...
User types an address. Geocode it, resolve to BBL and BIN, then fan out in
parallel to every dataset in the registry that carries bbl or bin (fall back
to a 100m radius on lat/lng for datasets that only have coordinates, using
within_circle in SoQL). Render one page:
- Header: normalized address, borough, BBL, BIN, PLUTO facts (year built,
  floors, units, zoning, owner name, lot area).
- One card per dataset showing count, most recent 5 records, and a "see all"
  that expands to a paged, sortable table of every matching record for that
  dataset, plus a "show on map" action that opens Feature 2 centered on the
  building with that dataset as the active layer.
- A unified timeline strip merging events across all datasets by date,
  color coded by source.
- Loading must be progressive: cards populate as each request returns.
Acceptance: any Manhattan or Brooklyn address resolves and shows at least
PLUTO plus 311 within 3 seconds on a normal connection. Failed datasets show
an error card rather than blocking the page.

FEATURE 2: LAYERED HEX MAP WITH TIME SCRUBBER
Route: /map?layers=...&from=...&to=...&res=...&bbox=...
- Left panel: pick any number of point datasets from the registry as layers.
  Each layer gets a color and an optional SoQL $where filter the user can
  edit (e.g. complaint_type='Noise - Residential').
- For the current viewport and date range, fetch points with within_box and a
  date range on the dataset's primary date column, capped at a sane $limit
  with a warning when the cap is hit. Aggregate client side into H3 hexes
  (resolution 8 to 10 depending on zoom). Render as a MapLibre fill layer
  with opacity by count. Hover shows count per layer.
- Time scrubber at the bottom: a range slider by month. Dragging re
  aggregates from already fetched data when possible and refetches only
  when the range leaves what is cached. A play button animates month by
  month.
- Secondary views: hour of day and day of week histograms for each active
  layer, computed from the fetched points.
- Accept an ad hoc layer via URL: a dataset ID plus a full SoQL string, so
  Feature 3 can hand results here.
Acceptance: three layers at 50k points total stays interactive on the
scrubber. Sharing the URL reproduces the exact map.

FEATURE 3: NATURAL LANGUAGE TO SOQL
Route: /ask?dataset=...&q=...
- User picks a dataset and types a question. Fetch that dataset's column
  metadata (names, types, descriptions) from the views endpoint and build a
  system prompt containing: the SoQL grammar subset we support ($select,
  $where, $group, $order, $limit, date_trunc_ym, date_trunc_ymd, count,
  sum, avg, within_circle, within_box), the column list, three worked
  examples, and the instruction to return only JSON of the form
  {soql: string, explanation: string, chart: "table"|"bar"|"line"|"map"}.
- Call the Anthropic Messages API from the client (model claude-sonnet-4-6,
  the key is handled by the environment; do not prompt for one).
- Show the generated SoQL in an editable box, run it, render the result in
  the suggested chart type, and show a one paragraph explanation of what
  the query does. Every edit and rerun updates the URL.
- Add an "open on map" handoff to Feature 2 when results carry coordinates.
- If the query errors, feed the SODA error message back to the model once
  for a repair attempt, then surface the error.
Acceptance: "how many noise complaints per month in Bushwick in 2025" on
311 produces a correct date_trunc_ym grouped query and a line chart.

SHARED UI
- Top nav: Building, Map, Ask. A global address search box in the nav that
  routes to Feature 1.
- Every view has a "Copy link" and "Show query" control that reveals the raw
  SODA URL(s) being called.
- Dark and light theme via prefers-color-scheme.

NON GOALS
- No accounts, no saved queries beyond the URL, no server.
- No write operations of any kind.
- No client side SQL engine or cross dataset joins beyond what SoQL and the
  address fan out already provide.
- Do not build a generic Socrata catalog browser; the registry is curated.

DELIVERABLES AND ORDER
1. Project scaffold, SODA client, registry with live schema fetch, URL
   state helper. Commit.
2. Feature 1 end to end. Commit.
3. Feature 2. Commit.
4. Feature 3. Commit.
5. README with architecture notes, how to add a dataset to the registry,
   and known data caveats (311 duplicates, DOB BIS vs DOB NOW split, PLUTO
   version differences).
Keep each commit runnable. Prefer small, typed modules over clever
abstractions. When a SODA call fails, print the full URL in the console.