DEV Community

US Directory
US Directory

Posted on

Normalizing U.S. Company Names Before You Match Them

Company names from different sources rarely agree character for character. "The Acme Group, L.L.C.", "ACME GROUP LLC" and "Acme Group" could be one entity or three. A normalized name gives you a stable comparison key, as long as you remember what the key discards.

What usually needs cleaning

Case and spacing. Pick one case, trim the ends, and collapse repeated spaces.

Punctuation. Periods, commas, and quotes are noise. Curly and straight apostrophes should end up identical, or be removed. An ampersand is safest as the word "and."

Accents. "Peña" and "Pena" should compare equal. Unicode normalization followed by an ASCII fold handles the common cases.

Legal endings. LLC and L.L.C., Inc. and Incorporated, Corp. and Corporation, Co. and Company, Ltd. and Limited, plus LP, LLP, and PLLC. Map the spellings to one short form and move it out of the name into a field of its own. It tells you the entity type, and "Acme Inc" and "Acme LLC" can be two separate legal entities.

A leading "The". Sources disagree on whether to keep it, so the key should not depend on it.

Trade names. A string like "Acme Group LLC dba Bright Path" holds two names. Split on dba, d/b/a, aka, or fka and keep both parts, labeled. Matching on the trade name alone can attach a record to the wrong legal entity.

Same name, different state

Identical names across states are common, because each state checks name availability only against its own registry. A key that ignores the state will merge strangers. Put the state, or the registry the record came from, into the key, and treat a cross-state match as a lead to confirm with other fields.

A teaching example

This is a teaching example, not production code. It returns the state, the core name, the entity-type ending, and any trade name found after dba, aka, or fka.

import re
import unicodedata
ENDINGS = {"INCORPORATED": "INC", "CORPORATION": "CORP", "COMPANY": "CO", "LIMITED": "LTD"}
ENDINGS.update({e: e for e in ("LLC", "INC", "CORP", "CO", "LTD", "LP", "LLP", "PLLC")})
ALIAS = re.compile(r"\b(?:D[./]?B[./]?A|A[./]?K[./]?A|F[./]?K[./]?A)\b\.?", re.I)

def words(text):
    text = unicodedata.normalize("NFKD", text).encode("ascii", "ignore").decode()
    text = text.upper().replace("&", " AND ").replace(".", "").replace("'", "")
    return re.findall(r"[A-Z0-9]+", text)

def company_key(raw, state):
    legal, *alias = ALIAS.split(raw, maxsplit=1)
    core = words(legal)
    ending = ENDINGS.get(core[-1], "") if core else ""
    while core and core[-1] in ENDINGS:
        core.pop()
    core = core[1:] if core[:1] == ["THE"] else core
    return state.upper(), " ".join(core), ending, " ".join(words("".join(alias)))
Enter fullscreen mode Exit fullscreen mode

Two calls show the behavior, including an edge case:

company_key("The Acme Group, L.L.C. d/b/a Bright Path", "tx")
# ('TX', 'ACME GROUP', 'LLC', 'BRIGHT PATH')
company_key("Peña & Co.", "or")
# ('OR', 'PENA AND', 'CO', '')
Enter fullscreen mode Exit fullscreen mode

The second result leaves a dangling AND, because "& Co." is really part of how that firm writes its name. Rules like these are blunt, and every ruleset produces cases like this one.

Order matters as well. Split off the trade name before removing punctuation, or "d/b/a" falls apart into loose letters that look like part of the name.

To keep the rules honest, build a small table of pairs you know are the same entity and pairs you know are not, and rerun it whenever you change a rule. A fix for one case often breaks another.

What the key throws away

Normalization is lossy on purpose. Store the raw name next to the key, keep the ending and the trade name as separate fields, and never treat an equal key as proof of identity. Confirm with something the name cannot fake: a registry ID, a formation date, or a principal address.

This post was drafted with the help of AI tools.

Top comments (0)