Hoodie Bev
Scrape & data console

DoorDash slug titles — what was built (2026-10-14)

Status: built, applied and scheduled. This started as a proposal; the first design got the city wrong, so this records what shipped and why.

The problem

785,193 of the master's ~936k outlets (84%) are single-record DoorDash stores. DoorDash publishes no store name, only a URL slug — lowercase, the store's city appended: busa wine and spirits woburn. They were titled exactly that.

What the first design got wrong

It assumed the sitemap's recorded city was the store's city, because it matched the slug's tail. It is only the slug's last word (creek, never Battle Creek; 0 of 10,322 multi-word cities were whole). Stripping by it left Burger King Battle, Pizza Hut Lake Havasu, 7 Eleven New. The error was caught by the dry run on live data, before anything was applied.

How it works now (slug_title.py, master_outlet.py)

Every slug ends with its city, so the last word is always part of it; the unknown is the city's length. The full city comes from, in order:

  1. the store's own address page (doordash_addr.city, ~78k stores; equals the slug's tail 99.9%);
  2. the longest tail of the slug that is a known city in that state — a ~55k-name gazetteer (city_centroids, doordash_addr, ubereats_stores, ca_outlets, hemp_retailers, retail_outlets) plus cities discovered from the slugs themselves (below); a - bucket holds stores with no state (Australia, NZ, UK);
  3. otherwise refuse and keep the raw slug. A wrong strip is worse than a slug.

Refusals: city-unknown, city-prefix-left (a name left ending in a city-starting word: "Ace Hardware South"), dangling-word ("Coffee Shop in Woburn": the name may end with the city), city-not-at-tail.

City discovery (discover_cities, table slug_city_learned)

A chain settles a city's length: "subway auckland" and "mcdonald's auckland" both leave "auckland" once a brand name is removed. A brand is a cleaned name seen in >= 25 cities. A city is learned only when >= 2 distinct chains agree, it is not already known, it is not a lone generic tail (valley, park), and a multi-word one is not led by a business word (liquor calgary). Hold-out test (30% of known cities hidden, rediscovered, scored on 22,098 addressed stores): 95.3% right, 0.11% wrong, 4.6% refused. Three plausible guards measured worse and were dropped (a name-frequency head guard, chain-direct at any support, a place-word-head requirement) — recorded in CLAUDE.md.

Casing v2 (CasingDict)

Title Case loses brand casing. The master already holds real spellings, so a slug whose name matches one takes it: an order-sensitive whole-name key (Food Giant is not Giant Food), else acronym/brand casing token by token. ALL-CAPS licence names and sloppy spellings are never borrowed. Leave-one-out exact casing 86.0% -> 98.9%.

What was measured, live

Leak-free city strip, 60,000 addressed stores (true city held out)97.11% exact, 0.04% different, 2.85% refused
After discovery + casing769,187 of 785,193 cleaned (98.0%); 16,006 keep the raw slug
Casing v2 live dry run47,222 titles changed, 0 changed a real word

Honesty rules

  • The record is never rewritten. master_outlet_member.name stays the slug; only master_outlet.display_name changes.
  • Derived titles are marked. master_outlet.display_name_basis = record / slug-cleaned / slug-raw; the Outlets list shows a "derived" marker and the detail page explains it.
  • Re-derived every run, written only if it differs — a better rule repairs what an older one wrote, and a second run is a no-op. updated_at is untouched, so "recently changed" is not a mass edit.
  • Nothing that matches or links reads the cleaned title.

Operation

  • python -m hoodie_bev.master_outlet --clean-slugs [--apply] — dry run first: counts, 30 before/after pairs, suspects, refusals.
  • --discover-cities [--apply] — dry run first.
  • Scheduled weekly as registry source slug-cities (default lane, ~2 min): discover, then re-derive every title. A discovery that would keep < 50% of the learned cities is a failed read and is refused (table kept, run degraded).
  • The daily master-outlet build re-cleans the outlets it touches.

Known limits (pinned in tests)

& read as and; a few hyphens and acronyms with no real spelling on file; non-US cities that no chain confirms stay raw (9,242 city-unknown); two learned cities carry an extra word ("catering irvine") because the real city is already known and a stricter rule measured worse on the hold-out.