Skip to content

[Demo] Support cross-streets as an alternative location input#973

Draft
saadabdali wants to merge 3 commits into
devfrom
sa/cross-streets
Draft

[Demo] Support cross-streets as an alternative location input#973
saadabdali wants to merge 3 commits into
devfrom
sa/cross-streets

Conversation

@saadabdali

@saadabdali saadabdali commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Consider this a tech exploration / tech demo for a feature supporting cross-streets as an alternative input for incident location.

Screen.Recording.2026-06-10.at.20.04.02_small.mp4

User-visible changes

  • The location autocomplete dropdown now accepts either a street address OR an intersection (e.g. 16th & Mission). The autocomplete results show little icons to indicate whether a given result is an address or an intersection.
  • The resulting form now has two modes. If you chose an address, you'll see the Address line 2 field. If you chose an intersection, you'll instead see two fields for Street 1 and Street 2.
  • We show a mic button on the Location field. The officer can tap to speak an intersection e.g. "14th and Mission," it's transcribed and matched to a real intersection, and the field fills in. (Sorry the display here is slightly ugly/misaligned, wasn't too focused on the look for now.)
  • If the voice transcription result matches more than one intersection, the app presents all the matches and lets the user tap the correct one. If transcription fails to resolve to any known intersection, we show the raw transcribed text, and nudge the user to enter manually.
  • All the downstream consumers of the address data gracefully handle both modes. (Incident detail view, custody detail, the map-link URLs, the arrests report, and the 849b PDF.)

Schema changes

It felt like the best way to store this was to create some new fields and also store an explicit flag indicating which type of location this is.

Added these fields to Incident:

  • locationType enum (ADDRESS | INTERSECTION, default ADDRESS). This is the discriminator (tells you whether this Incident uses the existing address fields, or the new intersection fields.)
  • street1
  • street2
  • intersectionId (we match to an intersection from the the DataSF CNN)

Didn't remove or modify any existing fields.

Had to update the way we validate addresses.

To read older records (which won't have a locationType) can assign a default value on read, or use migration to add locationType to older entries.

Dependencies

  • Added: natural (^8.1.1) — an NLP toolkit
  • New uses of dependencies we already had:
    • Re-used AWS Transcribe for voice transcription
    • DataSF Socrata API as the data source for a one-time build script (to create an index of named intersections)

Elbow grease to make the voice stuff work

It took a lot of iteration and messing around to get the voice stuff to be even moderately reliable. I remain optimistic. On the one hand, we had to make a lot of spot-fixes to solve individual error cases. On the other hand, SF's street network is of bounded size, and sometimes elbow grease is what you need to make something great.

(build-intersection-data.js):

  • fetches DataSF's ~21k intersection-permutation rows
  • dedupes to ~11,006 unique intersections
  • derives a canonical street list
  • tags each intersection with its neighborhood (point-in-polygon against SF neighborhood GeoJSON, ~99% match).
  • writes a flat JSON, which the app loads on start.

A matching pipeline to try and reliably turn transcribed voice into a known intersection:

Layer Problem it solves Example
Suffix/prefix normalization Officers say "Mission," DataSF stores "MISSION ST" strips/expands ST/AVE/BLVD, handles "THE Embarcadero", BAYSHORE→BAY SHORE aliases
Ordinal-word expansion Transcribe emits "Third", DataSF stores "03RD" (digits have no phonetics) "Twenty-Fourth" → "24TH", zero-pads to "03RD ST"
Phonetic fallback (Double Metaphone + Levenshtein) STT vowel-shift mishears "Junipero Sarah" → Junipero Serra; "fulsome" → Folsom
Candidate ranking Popular streets buried under compound siblings "Mission St" was alphabetically cut off behind "Mission Bay Blvd North…" — now exact-base matches rank first
STT-fusion recovery Speech bigrams fuse into real words "14th and Mission" → Transcribe hears "14th admission" → peel "ad-" → "mission" is a known street → split

A benchmarking harness (bench-intersections.js, bench-datasf-intersections.js): a curated ~30-intersection test set across categories (arterials, Spanish names, numbered streets, multi-word) used to compare AWS's geocoder vs. our local index and to regression-check tuning changes.

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.

1 participant