Two independent creation paths named businesses after the city they were DISCOVERED from rather than where they are, putting the wrong town in 239 URLs. The pattern, the fix, and the audit.
The bug shape: a creation path knows two cities — the one it is looking from and the one the place is in — and uses the wrong one. It happened twice, independently, in two subsystems written months apart. Assume it will happen a third time.
| Path | Used | Should have used |
|---|---|---|
parCreateSkeleton() |
the ANCHOR business's city (the page the visitor was on) | the place's own address |
| Prospector discovery | the SWEEP CENTER's city ($resolvedCity = $center['city']) |
the place's own address |
Results in the wild: a Brooklyn bowling alley at melody-lanes-morristown-nj.doozer.site, a Brentwood restaurant at blaze-pizza-nashville-tn, a New York restaurant at tao-downtown-restaurant-jersey-city-nj.
The context city is almost right. Most results of a Morristown sweep really are in Morristown, so the bug hides in the minority and every spot-check passes. It only shows up when you compare the slug against the address systematically. 239 of 3,439 domains (1,304 of which use the city-state slug shape) named the wrong town before anyone noticed.
It is also invisible in the product: the page content, title and schema were all correct, because those read businesses.formatted_address. Only the URL lied.
A city is a fact about the place, never about how you found it. Discovery context is
a hint for routing and a provenance note. It must never name anything.
Par-create records this correctly for provenance: custom_settings.discovered_on keeps where a skeleton was seen, while city comes from the address. That is the right split.
parCreateCityStateFromAddress($addr) in par-create.php parses the place's own city and state from the address we already hold, and returns ['',''] rather than guessing when it cannot parse confidently, so callers fall back rather than invent. Both paths call it now.
Watch the address shapes: Google returns both "912 Red River St, Austin, TX 78701" and "222 W 1st Ave, Plentywood, MT 59254, USA". Strip a trailing country, then take the part before the ST ZIP tail. Reject a candidate that starts with a digit, which means you grabbed a street line.
dz slugaudit — scans every active domain, extracts a trailing -city-state slug, and compares it to the address. Reports, dry-runs, repairs.
It splits the fix by risk, and that split is the important part:
be a deletion plus a creation. Resolved in v2.2.541: see below. All 22 are renamed and the network now audits clean at 0.
includes/domain-redirects.php. A retired host keeps its domains row with status='disabled' (already means "not serving", so no migration) plus custom_settings.redirect_to, and domainRetiredRedirect() 301s it.
Two properties make it safe to leave switched on:
precisely where the configure wizard used to load. A live page never reaches it. Adding a query to all 3,400 sites' hot path to serve a handful of retired hosts would have been the wrong trade.
host we actually run. We are the only writer today, but a redirector that trusts a database string is how you become somebody else's phishing hop later.
Rename procedure: UPDATE the live row's domain (keeping id, page-view history and sitemap membership), then INSERT a disabled row for the old host carrying the redirect. Then push the new URL to IndexNow so Bing-backed search sees the move.
A correction worth keeping. The original justification for leaving indexed domains alone included "the orphan falls into the configure flow and offers a stranger the chance to configure somebody's business." That was wrong: runRootDomainGuard() already returned a 302 with x-doozer-lockdown: blocked-subdomain for unregistered subdomains. The real value of retirement is the permanent 301 telling a search engine the address moved, not protection that already existed. Verify the claim, not just the conclusion.
When a naming bug is found in one subsystem, grep for the same shape everywhere else before declaring it fixed. The instinct that caught this was simply asking whether the bug had happened before. Both paths had it; only one was found by working on it directly.
Related: [[the-observed-world]], [[fact-guard-identity-numbers]], [[dev-api]].