“The same company” is a modelling problem, not a string problem
Every entity resolution project starts the same way: someone tries Levenshtein, gets to about 70%, adds a stopword list, gets to 78%, and then spends two years discovering that the last 20 points are not in the string.
The plateau is real and it is measurable
We ran the obvious baselines over the SPOT-Bench v4 public subset. These are not strawmen; they are tuned, with legal-form stopwords, transliteration and token sorting.
| Approach | Top-1 | Where it fails |
|---|---|---|
| Levenshtein on raw name | 61.4% | Legal forms, truncation |
| Token-set ratio + stopwords | 74.2% | Renames, similar names |
| Embedding similarity | 78.1% | Confidently merges siblings |
| Embedding + country filter | 83.6% | Sole traders, young companies |
| spotit, four scorers | 97.3% | See the methodology |
The interesting number is the third row. Embeddings do not beat token-set matching by much, and they fail *worse* — a fuzzy score of 0.4 is a visible signal to route to review, whereas an embedding puts two genuinely different sibling companies at 0.91 and looks certain about it.
Three failures no string method can reach
1. The name changed
Nordwind Spedition GmbH became Nordwind Logistik GmbH on 2 April 2019. Same HRB 148902, same court, same company, continuous legal personality. Any system keyed on the name now holds two companies, and the older of them silently stops receiving updates.
Renames are not rare. In our German ingest, 61,400 name changes print in a year against roughly 6.4M active companies — about 1% annually, which compounds into 8% of a ten-year-old CRM.
2. The names are genuinely the same
There are 41 active companies named exactly “Nordwind GmbH” in the German register, in 38 different communes. No amount of string cleverness separates them, because there is nothing in the string to separate. Geography does it; so does a register key; so does a domain.
3. The input is not a name
A quarter of real inputs carry an address glued into the name field, a truncation at 40 characters, or an OCR substitution. String similarity degrades smoothly here, which sounds good and is not: it produces confident wrong answers rather than abstentions.
What a register key does instead
A register number is issued by an authority, is unique within that authority, and survives every rename. When one is present in the input — or derivable from a VAT number, an LEI or an EUID — matching stops being a similarity problem and becomes a lookup.
That is why the register-key scorer carries about half the weight of an accepted match, and why passing a register or VAT number as a hint is worth 9.4 points of top-1 on its own. Everything else in the pipeline exists for the cases where no key is available.
Fuzzy matching is what you do when you have failed to find an identifier. It should be the fallback, not the architecture.
The practical version
- Store an identifier, not a name. Once you hold entity_id, every later read is a key lookup and matching never runs again.
- Pass every hint you have. Country is worth 4.1 points; an address fragment 2.8; a domain 3.2. They are free.
- Treat a rename as an event, not a new row. Watch delivers entity.name_changed, and handling it is a dozen lines.
- Keep a review band. A system with no abstention has simply moved its errors somewhere you cannot see them.
The full scorer breakdown is in the matching guide.
Try it against your own data
The Developer plan is free forever and needs no card. 2,500 credits is enough to answer the only question that matters: does it resolve your records.