The problem
The same building carries a different ID at every supplier. Resolve one wrong and an entire search region returns zero hotels — the user sees "no availability", not an error.
The obvious fix is joining on the industry GIATA ID. A direct join produced almost
nothing: 2,639 of 2,773 GIATA IDs were absent from our table, because only
362,653 of 1,896,030 rows carried a giata_id at all.
The approach
Bridge through another supplier. For each GIATA ID, take the codes it carries for other suppliers, find our row through any one of those columns, then verify the candidate by hotel-name similarity and lat/long distance before writing — with the number of independent suppliers agreeing used as the confidence score.
Name matching groups by country to cut the comparison space, filters by distance
before calling the expensive function, uses a configurable threshold, and runs a
TF-IDF/n-gram model that strips only the two most generic tokens and learns
weights for the rest — keeping fuzzball as the fast fallback.
Identity is transitive, so lookups run over a Neo4j SAME_AS graph with
variable-depth traversal.
Every campaign: dry-run first → writes classified fill-NULL / overwrite / skipped → each written row re-read to verify → re-run dry to prove idempotency.
The result
Largest single campaign write: +109,786 rows (98,588 fills, 11,198 confident overwrites) against 186,593 rows deliberately left unchanged.
Quality: 1,668 of 1,702 new mappings confirmed by two or more independent suppliers (median 7), mean name similarity 94.9%, and 1,499 within 100 m.
And the team made a policy decision: only supplier-evidenced identity enters the table — a name plus coordinates is a hint, not evidence. Enforcing it meant rolling back an 11,953-row name-and-geo-only campaign row by row, with the post-rollback state verified at 286,105 non-null rows. That rollback is not a cleanup anecdote; it is the evidence policy being enforced against my own work.