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 "45 Main St, Sussex, NJ 07461, 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.
Spotcheck 01 found the same poison still live in a surface this note had called safe. 257 businesses carry a city column that disagrees with their own geo-verified formatted_address — Biddy O'Malley's is filed under Hawthorne while its address and its coordinates are both Northvale, fifteen miles away.
The write path is genuinely repaired: 256 of the 257 were created in July 2026 and zero have appeared since the fix. This is pure legacy residue, so the remedy is a backfill and nothing more — no code change, no API cost, because the correct address and coordinates are already on every affected row.
Correction to this note. It says the bug was "invisible in the product... the page content, title and schema were all correct, because those read businesses.formatted_address. Only the URL lied." That is no longer true, and may never have been true of the agent surfaces:
| surface | reads | Biddy shows |
|---|---|---|
| page copy, chip | formatted_address |
Northvale ✅ |
Atlas entity.address |
formatted_address |
Northvale ✅ |
Atlas entity.city |
the city column |
Hawthorne ❌ |
Atlas entity.name |
name + city column |
"Biddy O'Malley's (Hawthorne, NJ)" ❌ |
llms.txt summary |
the city column |
"in Hawthorne, NJ" ❌ |
So the machine-readable feed contradicts itself — its geo says one town, its name says another — and an agent citing it sends people to the wrong place. The human page is right and the agent surfaces are wrong, which is exactly backwards, and only a dual-surface pull catches it.
Extend the rule: a city is a fact about the place, **and every surface must read it from
the same place the fact lives.** Fixing the writer does not fix the rows already written,
and "the schema is fine" is a claim to re-verify, not inherit.
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.
And when you fix a writer, ask separately what it already wrote. A fixed path and a clean table are two different pieces of work; this one shipped the first and skipped the second for six weeks.
Related: [[the-observed-world]], [[spotcheck]](../processes/spotcheck.md).