You paste a list of company names into a lookup tool. Most rows come back right. A few come back confidently, cleanly, and completely wrong. Then someone emails the wrong company. This is the single most common failure in company data work, and it is not random. It fails in patterns you can predict and mostly fix.
SHORT ANSWER
Company name lookups return the wrong company because a company name is not a unique identifier. Thousands of real businesses share names like Bolt, Mercury, Atlas and Apex. Matching systems rank results by relevance, not by what you meant, so a confident top result can still be the wrong business.
The fix is three steps: normalise the name before you search, score how well the returned name matches what you typed, and never auto-accept a weak score. Everything below is how to do each one.
1. Why exact matching fails immediately
Try to match company names with == and you fail on the first row. These are all the same company:
- Apple
- Apple Inc.
- Apple Inc
- APPLE INCORPORATED
- Apple, Inc.
- apple inc.
And these are all different companies that a naive matcher will happily confuse:
- Bolt (ride hailing, Estonia)
- Bolt (payments, United States)
- Bolt Threads (materials, United States)
So you need matching that is loose enough to see through punctuation, casing, and legal suffixes, but tight enough to not merge three unrelated businesses. That tension is the whole problem. Fuzzy matching is how you manage it, not how you eliminate it.
Fuzzy matching means comparing two strings and getting back a similarity score between 0 and 1 instead of a yes or no. “Apple” against “Apple Inc” might score 0.8. “Apple” against “Microsoft” scores 0. You then decide what score is good enough.
2. The five patterns that cause almost every bad match
In practice, bad matches are not evenly distributed. They cluster into five types. Knowing which one you are looking at tells you how to fix it.
| Pattern | Example | What goes wrong | Fix |
|---|---|---|---|
| Legal suffix noise | Acme Technologies Private Limited | Database stores the trading name, “Acme” | Strip suffixes before searching |
| Short shared names | Bolt, Mercury, Atlas, Apex, Nova | Several real companies, no way to tell from the name alone | Never auto-accept; read alternatives |
| Parent vs brand | Louis Vuitton vs LVMH | Resolves to the parent, or to a sibling brand | Search the brand for products, the group for corporate data |
| Rebrands | notion.so became notion.com in June 2026 | Old list, stale domain, silent failure | Re-run lists older than a few months |
| Not in the database | A three-person local firm | Returns nothing, or returns something similar and wrong | Manual search is faster than fighting it |
The dangerous one is the second row: Legal suffixes and rebrands announce themselves. Short shared names do not. “Bolt” returns a clean, confident, single result, and there is nothing in the response telling you three other companies also answer to that name.
3. Step one: normalise the name
Normalising means reducing both strings to a plain comparable form before you compare them. Skip this and every later step works on noise.
A workable normaliser does four things: lowercase everything, strip punctuation, collapse repeated whitespace, and trim. That is it. Do not get clever.
javascript
function normalise(s) {
return String(s || '')
.toLowerCase()
.replace(/[^a-z0-9 ]+/g, ' ') // punctuation and symbols out
.replace(/\s+/g, ' ') // collapse whitespace
.trim();
}
normalise('Apple, Inc.'); // "apple inc"
normalise("O'Reilly Media"); // "o reilly media"
Stripping legal suffixes
This is the single highest-value line of code in company name work. Legal suffixes are how the company registers, not how the database stores it, and leaving them in noticeably lowers your match rate.
javascript
const SUFFIXES = /\b(inc|incorporated|corp|corporation|co|company|ltd|limited|llc|llp|plc|gmbh|ag|sa|srl|spa|bv|nv|ab|oy|pty|pvt|private|group|holdings|international|technologies|solutions|services)\b\.?/gi;
function stripSuffix(name) {
const out = String(name).replace(SUFFIXES, ' ').replace(/\s+/g, ' ').trim();
return out.length >= 2 ? out : String(name).trim(); // never strip to nothing
}
stripSuffix('HubSpot Inc.'); // "HubSpot"
stripSuffix('Acme Technologies Private Limited'); // "Acme"
stripSuffix('Tata Motors Limited'); // "Tata Motors"
Note the guard on the last line: Some companies are genuinely called “Group” or “Holdings”. Without that check you strip the name to an empty string and search for nothing. Small bug, silent, ruins a batch.
What not to normalise away
Resist the urge to keep going. Removing “the”, expanding “&” to “and”, or stripping numbers will merge companies that should stay separate. Normalise conservatively, then let the score handle the rest.
4. Step two: score the match with a real algorithm
Once both strings are clean, compare them. Do not write your own heuristic. Use an established string similarity measure. The Dice coefficient over character bigrams is a good default: fast, no dependencies, works well on short strings like company names.
Bigrams are just overlapping two-character pairs. “apple” gives you ap, pp, pl, le. Dice counts how many bigrams the two strings share, relative to how many they have in total.
javascript
function similarity(a, b) {
a = normalise(a); b = normalise(b);
if (!a || !b) return 0;
if (a === b) return 1;
if (a.length < 2 || b.length < 2) return 0;
const pairs = new Map();
for (let i = 0; i < a.length - 1; i++) {
const bg = a.slice(i, i + 2);
pairs.set(bg, (pairs.get(bg) || 0) + 1);
}
let hits = 0;
for (let i = 0; i < b.length - 1; i++) {
const bg = b.slice(i, i + 2);
if (pairs.get(bg) > 0) { pairs.set(bg, pairs.get(bg) - 1); hits++; }
}
const dice = (2 * hits) / (a.length - 1 + b.length - 1);
// Whole-word prefix bonus. "Zoom" vs "Zoom Video Communications"
// scores near zero on raw bigrams but is probably right.
// Deliberately capped below the High threshold, because "Salesforce"
// is also a prefix of "Salesforce Ben", which is a different website.
const [short, long] = a.length <= b.length ? [a, b] : [b, a];
if (long.startsWith(short + ' ')) {
return Math.max(dice, short.length >= 6 ? 0.78 : 0.60);
}
return dice;
}
Why the prefix bonus matters
Raw Dice punishes length differences hard. “Zoom” against “Zoom Video Communications” scores badly even though it is almost certainly the same company. Without a correction you would reject correct matches constantly.
But the correction has to be capped. Look at what happens if you let a prefix match score High:
| You typed | Returned | Correct? |
|---|---|---|
| Zoom | Zoom Video Communications | Yes |
| Notion | Notion Labs | Yes |
| Salesforce | Salesforce Ben | No, that is a news site |
| Mercury | Mercury Systems | Maybe, three companies fit |
The last two rows are why the bonus stops in the middle band. A prefix match means worth checking, never trust it.
5. Step three: set thresholds and review the middle
A raw number helps nobody at 3 pm on a Friday. Convert it into three bands and act on the band, not the number.
javascript
function band(score) {
if (score >= 0.85) return 'High'; // names are effectively identical
if (score >= 0.50) return 'Medium'; // partial match, open it
return 'Low'; // names differ a lot
}
What to do with each band:
- High. Names are essentially the same. Safe to import in bulk. Spot check a random ten percent anyway.
- Medium. This is where the value is. Something matched, but not cleanly. A human glance takes two seconds and catches nearly every bad row.
- Low. Treat as a lead to investigate, not an answer. Often the company is simply not in the database.
Show the matched name, not just the domain. This is the cheapest accuracy improvement available. A bare domain like
boltfinancial.comtells a reviewer nothing. “You typed Bolt, we matched Bolt Financial” is instantly, obviously checkable. Two columns instead of one, and your error rate drops.
Keep the alternatives
Most lookup APIs return several candidates, and most tools throw away everything except the first. Keep the next three. For ambiguous names, the right company is very often sitting in position two, and surfacing it turns a wrong answer into a one-click correction.
6. Measuring accuracy on your own list
Every article on this topic quotes an accuracy percentage. Almost none say what data produced it, which makes the number close to meaningless. Accuracy depends on your list, not on anyone else’s.
So measure it. Take thirty to fifty companies from your real data, look up the correct domain by hand once, and run this.
python
"""Measure lookup accuracy on YOUR list.
1. Build truth.csv with two columns: name,correct_domain
2. Fill in 30 to 50 rows from your real data, verified by hand.
3. Point find_company_url() at whatever lookup you use.
"""
import csv
from collections import Counter
def clean_domain(d):
return (d or "").lower().replace("www.", "").strip().rstrip("/")
def benchmark(path="truth.csv"):
with open(path, newline="", encoding="utf-8-sig") as fh:
rows = list(csv.DictReader(fh))
bands, correct, misses = Counter(), Counter(), []
for row in rows:
expected = clean_domain(row["correct_domain"])
result = find_company_url(row["name"])
if result["status"] != "ok":
bands[result["status"]] += 1
misses.append((row["name"], expected, result["status"]))
continue
b = result["band"]
bands[b] += 1
got = clean_domain(result["domain"])
if got == expected:
correct[b] += 1
else:
misses.append((row["name"], expected, f"{got} [{b}]"))
total, right = len(rows), sum(correct.values())
print(f"Overall: {right}/{total} = {right / total:.0%}\n")
for b in ("High", "Medium", "Low"):
if bands[b]:
print(f"{b:7} {correct[b]:3}/{bands[b]:3} = {correct[b] / bands[b]:.0%}")
if bands["not_found"]:
print(f"\nNot in database: {bands['not_found']}")
print("\nMisses:")
for name, expected, got in misses[:25]:
print(f" {name:30} expected {expected:28} got {got}")
if __name__ == "__main__":
benchmark()
The per band breakdown is the part that pays for itself. If High rows come back right almost every time on your data, you can auto import them and review only Medium and Low. If High is unreliable for you, that tells you something important about your list before you send a single email.
One thing the benchmark will teach you
Separate a failed request from a missing company. Most scripts collapse both into “not found”. Then the API has an outage, every row comes back empty, and someone concludes the whole list is bad data. Three statuses, not two: ok, not_found, request_failed.
7. When fuzzy matching is the wrong tool
Being honest about the ceiling saves you a week of tuning that will not work.
| Situation | Use |
|---|---|
| Turning a marketing list into domains | Fuzzy matching, review the Medium band |
| Deduplicating your own CRM | Match on domain, not name. Domains are near unique, names are not |
| Legal entity identification, KYC, compliance | Official company registers and registration numbers. Not name matching |
| Merging two datasets that both have domains | Join on domain and skip name matching entirely |
| A company that rebranded last month | Nothing helps. Re-run the list |
The pattern in that table: whenever a stable identifier exists, use it. A domain, a registration number, a LEI or a ticker will always beat a name. Fuzzy matching is what you reach for when the only thing you have is a name, which is often, but it is a bridge to a real identifier, not a replacement for one.
Frequently asked questions
What is fuzzy matching for company names?
Comparing two company names and getting a similarity score between 0 and 1 instead of a simple yes or no. It lets “Apple” match “Apple Inc.” while still scoring “Apple” against “Microsoft” as zero. You then set a threshold for what counts as a match.
Which algorithm should I use?
Dice coefficient over character bigrams is a strong default for company names: fast, dependency free, and reliable on short strings. Levenshtein distance also works but is slower and punishes length differences harder. Whichever you pick, add a whole word prefix rule so “Zoom” matches “Zoom Video Communications”, and cap that rule below your High threshold.
Why does my lookup return a completely different company?
Because a company name is not a unique identifier. Bolt, Mercury, Atlas, Apex and Nova each belong to several real businesses. Lookup services rank by relevance, not by your intent, so the top result is the most popular match rather than the right one. Always read the alternatives for short names.
Should I strip Inc, Ltd and LLC before matching?
Yes. Databases store trading names, not legal names, so suffixes reduce your match rate. Just guard against stripping a name to nothing, since some companies really are called “Group” or “Holdings”.
What accuracy should I expect?
There is no honest universal number, which is why this page gives you a benchmark script instead of a statistic. Global brands resolve reliably. Short shared names frequently do not. Very small local businesses are often absent entirely. Run the script on thirty of your own companies, and you will have a figure that actually applies to you.
Can I just use AI to match company names?
A language model will handle context that string matching cannot, such as knowing that Meta and Facebook are the same organisation. It is also slower, costs money per row, and will confidently invent a plausible domain when it does not know. If you use one, still verify the domain and still score the result. The verification step does not go away.
Try it without writing code
Our free company URL finder applies everything on this page: it strips legal suffixes, scores every match with the algorithm above, shows the matched name next to the name you typed, and surfaces the alternative candidates so you can swap in the right one. Paste up to 25 names, export a CSV. No sign-up.