Malcolm X Is Not a Suffix

1 19
calendar_today agoschedule5 min read
— Originally published at ilia.ws

Parse Malcolm X with almost any name splitter and you get a suffix. X looks like a roman numeral, roman numerals are suffixes (John III, Henry VIII), so X becomes the suffix and the surname disappears. That's the kind of bug you only find once you already have a working parser, because it isn't a missing rule. It's two correct rules wanting the same token.

Last month I wrote about the casing fix in nameparser: the idea that a parser reading Jane Doe DDS should keep Doe as the surname and DDS as a credential, using letter casing as the signal that lowercasing throws away. Version 1.3.0 is the follow-up, and it is almost entirely about collisions like Malcolm X: tokens that two disambiguation rules both claim. nameparser is a maintained fork of the dormant theiconic/name-parser, by way of codebyzach/name-parser's PHP 8.3+ modernization; the contribution here is the casing and credential layer plus a confidence pass, not the parser core.

The credential that is also a name

JO ANDERSON PhD used to parse to first name J. Jo is a real short given name, but in all caps it also reads as a credential-run token, and the credential rule won. 1.3.0 keeps all-caps short given names intact beside a mixed-case salutation or credential, so Jo survives.

The mirror image is the credential that gets read as a surname. Jane DDS (a credential-only row with no comma) and John A. MD (an initial plus a credential) used to rewrite the trailing credential as the last name, because a token in the surname slot with nothing after it looks like a surname. Both now keep the credential in the suffix.

The collision gets worse with more than one credential. John Smith MD FACS used to keep MD and leak FACS into the name, because FACS is not in the dictionary and an unknown trailing token defaults to a name. Now a known credential anchors the tail and pulls the unknown one in with it, so both stay in the suffix. Parenthetical credentials were their own trap: Jane Doe (MD) used to parse (MD) as a nickname, since parentheses mark nicknames, and now reads it as a suffix. And a credential-only segment that lands before the given name, as in Smith, MD, John, is pulled out to the suffix so the first name stays John rather than becoming MD.

Casing is the tiebreaker for all of these, which means it fails exactly where casing carries no signal. Uniform all-caps registry data, or all-lowercase input, has nothing to disambiguate on, so an ambiguous trailing token defaults to being read as a credential. That's a hard limitation, not a bug, and it's why the library ships a confidence pass: getConfidence() flags a casing-undecidable row as ambiguous so it can go to a human instead of silently into your database wrong.

Word order that hides the surname

Surname-first input is its own minefield when a salutation and a credential sit on the same name. Dr. Kim Jong Un used to lose the surname to the title; Kim Jong Un, MD used to fall back to Western order. Both now keep surname Kim, and Dr. Kim Jong Un, MD correctly yields surname Kim, salutation Dr., and credential MD. The salutation and the credential were each pulling the parser back toward a different word order, and the fix is teaching it that a leading salutation and a trailing credential-only comma tail can coexist with a surname-first name.

The comma inside the nickname

A comma in a name usually means surname-first. So a comma inside a delimited nickname is a direct collision with the rule that reads commas as name-order markers. John (Bob, Jr) Doe and John 'Bob, Jr' Doe used to bisect on that inner comma and lose half the name. 1.3.0 keeps the nickname Bob, Jr whole, for bracketed, quoted, and custom multi-character delimiter pairs alike, and the same holds on the given-name side: Smith, John (Jack, Robert) keeps Jack, Robert intact.

Scripts with no casing at all

If casing is the signal, a script without upper and lower case has none. Wang, 李明 used to split 李明 into two initials, because the initials rule fires on short tokens and nothing told it these were not initials. 1.3.0 leaves caseless-script names intact as the first name. In the other direction, an elided surname particle like the one in 't Hooft used to read as an unterminated nickname or an initial; it now survives as the leading particle. Dutch honorifics (Dhr., Mevr., Mw.) also join the default salutation set, so Dhr. Jan de Vries reads the title as a salutation rather than a first name.

And the input that is trying to break you

A name parser sits directly on user-supplied strings, so an adversarial input is a real exposure, not a theoretical one. Kilobytes of unmatched quotes, 100 KB tokens of any case shape, megabyte-long rows: 1.3.0 parses all of them in linear time and bounded memory. Invalid-UTF-8 delimiter keys and empty whitespace sets that used to emit a compile warning on every parse are now ignored quietly.

Turning a wrong split into a review queue

Every collision above resolves on casing, and casing runs out of signal on uniform-case data. So the honest design isn't to pretend the parser is always right; it's to say when it's guessing. The confidence pass reads the same input the parser does and flags a row whose split hinges on a signal that is not there.

use Iliaal\NameParser\Parser;

$name = (new Parser())->parse('SMITH, JM');
$name->getConfidence();   // ['ambiguous' => true, 'notes' => [...]]
$name->getSource();       // 'SMITH, JM'  (the normalized input it judged)

SMITH, JM is exactly the case with no answer: with mixed case, Smith, JM splits JM into the initials J and M, because the mixed-case surname proves those are initials; all-caps SMITH, JM keeps Jm as a first name, because the caps could equally be a short given name. The parser has to pick one, and the confidence flag records that it had to. getSource(), new in 1.3.0, returns the normalized string the confidence was assessed against, so a batch importer can log precisely what it judged. The flag is advisory and opt-in; it never changes what parse() returns. On a million-row import, that is the difference between silently corrupting an unknown fraction of records and routing a known set of them to a person.

One caution the README states and this post repeats: ambiguous => false on an all-caps row is not a correctness guarantee. Clean credentials that are not also names (RN, PT, OD) are left unflagged on purpose, to keep review volume manageable, so a false there means "no name-colliding token," not "definitely right."

The seams where cases meet

None of these are new features. They're the boundaries between rules that were already there, the places where two disambiguation heuristics both reach for the same token. That's what this round of work looks like: not more cases, but the seams where cases meet. Casing decides most of them. Where it cannot, the confidence flag says so, and the row goes to review instead of into your data wrong.

composer require iliaal/nameparser

github.com/iliaal/nameparser

1 Comment

0 votes
🔥 Join developers growing publicly
Share your knowledge, build in public, and grow your developer presence with a global community.

More Posts

I Wrote a Script to Fix Audible's Unreadable PDF Filenames

snapsynapseverified - Apr 20

The Sovereign Vault — A Comprehensive Guide to Protocol-Driven AI

Ken W. Algerverified - Jun 4

Your AI Agent Skills Have a Version Control Problem

snapsynapseverified - Apr 22

Cavity on X-Ray: A Complete Guide to Detection and Diagnosis

Huifer - Feb 12

AWS Certifications Are a Building Block, Not the Final Destination

Ijay - Jun 16
chevron_left
563 Points20 Badges
17Posts
5Comments
4Connections

Related Jobs

View all jobs →

Commenters (This Week)

11 comments
1 comment
1 comment

Contribute meaningful comments to climb the leaderboard and earn badges!