Findings · 2026-08-20 · By VSNARY | Emmanuel Orta
Two schema nodes with the same @id are one node parsed twice, not two businesses
An @id is the identifier. Two records carrying the same one are, by definition, the same record appearing twice.
An entity collision is a real problem. If two business records on one page declare the same phone number, the same coordinates, or the same identifier, a retrieval system has two candidates for one identity and no way to choose. It is invisible to every schema validator — the markup is perfectly valid — and it is exactly the kind of thing worth reporting.
So we built a check: pull every LocalBusiness, Organization, Store and ProfessionalService node out of the JSON-LD, and flag any two that share a phone, an @id, a URL or a coordinate pair.
On one site it reported four collisions across two nodes, including a shared phone number and a shared @id. We wrote that up as a genuine defect.
The tell
Two records sharing an @id is not two businesses colliding. An @id is the identifier. Two records carrying the same one are the same record, appearing twice.
The check had found a duplicate of a single node and compared it against itself. Every field matched, because every field was the same field. Four “collisions” from one business.
Reading the live page settles it in one command:
business nodes in live HTML: 1 their @ids: ['https://example.com/#business'] distinct @ids: 1
One node. One identifier. The page was never wrong. The extraction was walking a JSON-LD @graph and yielding the same node twice — once as a top-level entry, once as a nested reference — and nothing downstream asked whether the two objects it was comparing were actually distinct.
The fix, and why it is not “ignore @id collisions”
The naive repair is to stop treating a shared @id as a collision. That would also delete the genuine case: two different businesses both handed the same identifier by a careless template. That is real, and worth catching.
The correct fix is upstream of the comparison. Deduplicate nodes by their full value signature — name, phone, street, latitude, longitude, URL, @id, type — before looking for collisions. Two objects identical in every field are one record; two records sharing an identifier but differing elsewhere are a genuine collision and still fire.
Checked across four sites afterwards, and the important result is the negative one: a site with two genuinely distinct business nodes still reports them as two, and still reports no collision between them. The dedupe did not suppress a real finding — which is the only evidence that a fix like this hasn't just turned the check off.
The process failure underneath it
The part worth keeping is not the bug. It is what “verified” meant when we announced it.
We had verified that the code path fired, that the finding was emitted with the right severity, that the evidence rows were populated, and that the report's own self-checks passed. All of that was true. None of it was evidence that the finding was correct, because every one of those checks ran against our own output.
Confirming the accusation required the one thing we had not done: fetching the accused page and counting the nodes by hand.
For a tool whose whole proposition is that it reports what is actually there, an unverified accusation is the worst possible defect — worse than missing the problem, because a false finding sends someone to edit markup that was already correct.
The rule we now apply: for any finding that accuses a site of something, verification includes independently fetching that site and confirming the accusation by hand. Not the code path. The claim.
Every figure above came out of this scanner.
Point it at your own domain and see the same measurements, free.