Complaint Entity Resolution
A consumer complaint names a business the way the consumer remembers it. This pipeline matches those names against 278,000 D.C. licence records. On a labeled benchmark, it made no wrong merges and counted complaints per business exactly 99.24% of the time. Anything it isn't sure about goes to a person.
Try it
The demo below runs on a sample batch. It may take a minute to wake if no one has used it recently.
The problem
Suppose a consumer writes that they had trouble at “the CVS on 14th.” The registry lists that store under its legal name and a street address. Variants like “cvs pharmacy #402” and “C.V.S.” describe the same store, too. (This example is illustrative; no real complaint data appears here.)
Before this tool, staff reconciled names and addresses by hand. Manual entry is slow and error-prone, so the office could not easily count how many complaints named the same business. That count is what tells an enforcement office where to look.
Where simpler approaches fail
The first baseline stripped each name to its core words and grouped exact matches. It almost never merged two different businesses (0.01% wrong merges), but it left variants of one business apart: 14.31% of true matches were missed.
Fuzzy matching closes that gap and opens a worse one. A standard fuzzy scorer gives a score of 90 to any name contained inside a longer one, so a short brand name looks like a strong match for an unrelated business. On the benchmark, that plateau caused 46 of 48 wrong brand merges. A wrong merge does more harm than a missed one, because it pins a complaint on the wrong business.
What I built
The pipeline is written in Python, with a Streamlit app for review. It runs in four stages:
- Clean and expand. The pipeline normalizes case, punctuation and spacing, then expands known aliases and nicknames.
- Find candidates. It fuzzy-matches each name against the 19,053-row D.C. registry and uses the address to choose between locations. Semantic search and Foursquare lookup exist as optional fallbacks and are off by default.
- Veto and score. Eight safety checks downgrade weak matches. Examples include a quadrant conflict, a street mismatch, and an entry with no address and no recognizable name. Every row then receives a confidence tier.
- Check housing. A later expansion widened the search beyond active storefronts to expired licences, housing records and the rest of the city’s licence data, about 278,000 records in all. Matches from outside the core registry appear as leads in a separate lane for a person to confirm.
The judgment call
The tiers encode one rule: a wrong automatic match costs more than extra review time. HIGH matches can be accepted in bulk, and MEDIUM matches need a quick look. REVIEW rows go to a person to decide, and NO_MATCH rows are marked as unresolved. I tuned the thresholds against frozen sets of labeled examples rather than toward a target share of automated matches. The strictest set, Tier A, passes 200 of 200.
Results
On the benchmark, the final pipeline merged no two different businesses (0.00% wrong merges), missed 0.90% of true matches, and got the per-business complaint count exactly right 99.24% of the time. The stem-only baseline missed 14.31%.
The tool does not yet run on real complaint data. A pilot on the office’s data is pending a volunteer agreement, so I can’t yet report time saved or staff feedback.
What it doesn’t do yet
Residential rentals make up 52.5% of active licences. The housing expansion lets the tool find them, but it only offers them as leads and never resolves them automatically. Landlord complaints matter to an access-to-justice office, so closing that gap is the next problem worth solving.