Fuzzy Matching Company Names: Why Your Lookups Return the Wrong Company

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.

PatternExampleWhat goes wrongFix
Legal suffix noiseAcme Technologies Private LimitedDatabase stores the trading name, “Acme”Strip suffixes before searching
Short shared namesBolt, Mercury, Atlas, Apex, NovaSeveral real companies, no way to tell from the name aloneNever auto-accept; read alternatives
Parent vs brandLouis Vuitton vs LVMHResolves to the parent, or to a sibling brandSearch the brand for products, the group for corporate data
Rebrandsnotion.so became notion.com in June 2026Old list, stale domain, silent failureRe-run lists older than a few months
Not in the databaseA three-person local firmReturns nothing, or returns something similar and wrongManual 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 typedReturnedCorrect?
ZoomZoom Video CommunicationsYes
NotionNotion LabsYes
SalesforceSalesforce BenNo, that is a news site
MercuryMercury SystemsMaybe, 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.com tells 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.

SituationUse
Turning a marketing list into domainsFuzzy matching, review the Medium band
Deduplicating your own CRMMatch on domain, not name. Domains are near unique, names are not
Legal entity identification, KYC, complianceOfficial company registers and registration numbers. Not name matching
Merging two datasets that both have domainsJoin on domain and skip name matching entirely
A company that rebranded last monthNothing 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.

Leave a Comment