Skip to content

Add a local-officials lookup to the council page - #138

Open
HaydenCash716 wants to merge 10 commits into
FoggedLens:masterfrom
HaydenCash716:find-local-officials
Open

Add a local-officials lookup to the council page#138
HaydenCash716 wants to merge 10 commits into
FoggedLens:masterfrom
HaydenCash716:find-local-officials

Conversation

@HaydenCash716

@HaydenCash716 HaydenCash716 commented Aug 6, 2026

Copy link
Copy Markdown

Closes #136.

The council page explains how to petition your local officials, but nothing on the site helps you figure out who they are — if you don't already know who to email, you probably won't email anyone. This adds a "Find Who to Contact" card at the top of /council. (This started as two stacked PRs; folded into one since they're really one feature. #139 is closed in favor of this.)

What you get

Type a city, county, or ZIP — with suggestions as you pause ("springfield" offers all the Springfields, "Newnan, Georgia" style labels with the county as a secondary line) — or hit the location button, and the card shows:

  1. The place's official website. The geocode result already carries osm_type/osm_id, so the component reads the boundary relation's website tag from the OSM API, falling back to Wikidata's P856 claim. It's pleasant that the answer usually comes straight out of OSM, and coverage is genuinely good — verified real official domains for Denver, NYC, KC, St. Louis, Winston-Salem, O'Fallon MO, London KY, Newnan GA (newnanga.gov), Coweta County (coweta.ga.us), and more.
  2. A council-roster search restricted to that domain (city council members contact site:www.denvergov.org) — city sites are where the actual rosters with names and emails live, so this lands one click away. Counties get commissioner wording throughout, since county commissions are who approve sheriff ALPR contracts.
  3. State legislators as people — official photo, name, party, chamber/district, tap-to-email and tap-to-call — via a new OpenStatesClient in the API (same shape as the other env-gated upstream clients, disk-cached by coordinates for 24h so the free-tier rate limit is a non-issue). Without an OPENSTATES_API_KEY configured, /officials 404s and the UI falls back to a plain legislator-finder link — merging this changes nothing until you add a key (free, ~2 min at https://open.pluralpolicy.com/accounts/profile/). If you'd rather not hold a key at all, this could instead bake Open States' bulk data into a district-keyed JSON like the alpr-counts pipeline — happy to rework it that way.

Backend additions to the Fastify API: /officials (Open States proxy), /geocode/reverse (for the location button; Nominatim /reverse at city zoom, cached like search).

Design constraints that shaped it

  • Nominatim's usage policy forbids per-keystroke autocomplete (and it doesn't prefix-match anyway), so suggestions fire after a ~600ms pause through the API's cached /geocode/multi, as word-level disambiguation. Suggestions clear on each keystroke so a fast Enter can't select a stale item.
  • Privacy: nothing runs on page load, nothing is stored. The location button rounds coordinates to two decimals (~1 km) in the browser before they go anywhere, and again server-side before Nominatim — there's a test on the rounding since it's the load-bearing part. The card's caption discloses all of it.
  • Vuetify gotcha for review: v-combobox commits typed text to its model on keystrokes and blur, so nothing is wired to string model updates — free-text searches happen only on Enter or the button; only a menu selection resolves through the model.

QA

Ran an adversarial battery (gibberish, emoji, <script> injection, SQL strings, ZIP+4, invalid/overlong ZIPs, "washington", "toronto" (correctly stays US: Toronto, Iowa), "brooklyn", street addresses, apostrophes/hyphens/diacritics, whole states, 300-char strings). Two real bugs surfaced and are fixed:

  • A non-place result (a road, a building) used to pass its own OSM website tag off as the city's — "1234" resolved to a highway and offered a transit agency as "Official Mount Vernon website". Now anything that isn't itself a municipality resolves to its containing city first, and only a municipality's own element can vouch for a website.
  • OSM/Wikidata tags are crowd-edited, so website URLs now pass an http(s)-only check (javascript:/data: rejected; scheme-less www.example.gov values, common in OSM, get https).

Correctness of the legislator lookup, verified without a key: for Newnan, GA, the city centroid and its ZIP both land in Senate District 6 / House District 70 per the Census Bureau's district boundaries, and Open States' public people repo has those seats held by Sen. Matt Brass and Rep. Lynn Smith — which is who a production lookup would return. (The upstream itself is exercised with stubbed fetch in bun test — 14 pass — plus a stubbed live server run in the browser; I don't have an Open States account of my own.)

bun run build-only passes; vue-tsc fails only on the App.vue/plugin-vue errors already broken on master (same ones #133 ran into).

@HaydenCash716

Copy link
Copy Markdown
Author

Pushed one improvement: when the city's website is resolved, the council search link is now restricted to that domain (city council members contact site:www.denvergov.org style) instead of a generic web search. City sites are where the actual rosters with names and emails live, so this usually lands one click from the right page. Falls back to the generic search when we don't have a domain.

@HaydenCash716

Copy link
Copy Markdown
Author

Two more additions after using it for a while:

City suggestions. Typing now offers a dropdown of matching places through the existing /geocode/multi endpoint — mainly useful for disambiguation ("springfield" offers all eight Springfields, you pick yours). Two constraints shaped it: Nominatim's usage policy forbids per-keystroke autocomplete, so suggestions only fire after a ~600ms pause and go through the API's 24h cache; and Nominatim doesn't do prefix matching anyway, so this is deliberately word-level suggestion, not character-level completion. Suggestions clear immediately as you type so a fast Enter can't grab a stale item from the previous query. One Vuetify gotcha worth knowing for review: v-combobox commits typed text to its model on keystrokes and blur, so nothing is wired to string model updates — free-text searches happen only on Enter or the button, and only a menu selection resolves through the model.

A use-my-location button in the field. It needs a small /geocode/reverse on the API (Nominatim /reverse at zoom=10, disk-cached like the search path). Coordinates are rounded to two decimals (~1 km) in the browser before they're sent anywhere, and again server-side before Nominatim — enough to find the city, never a precise position. The caption on the card discloses it. Denied/unavailable permission degrades to a "type your city instead" message; there's a test on the rounding and cache behavior in NominatimClient.reverse.test.ts since that's the privacy-load-bearing part.

Verified against a local run: suggestion select, Enter with free text, ZIP entry, and blur (which must do nothing) all behave; bun test passes (8) and the build is clean.

@HaydenCash716

Copy link
Copy Markdown
Author

Follow-up on the suggestions after real use: labels are now the "Newnan, Georgia" form (place + state as the primary line, county as a secondary line — the two-line pattern most location boxes use, and what the FlockHopper search already does), deduped, instead of echoing Nominatim's full display_name. And counties are now suggestible and handled properly — county commissions approve sheriff ALPR contracts, so "coweta county" resolves to Coweta County, Georgia with "Board of Commissioners"/"commissioners" wording and its own site-restricted search (found www.coweta.ga.us via the same OSM/Wikidata chain). Cities keep the council wording.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

How to Help Contact Information

1 participant