Repository navigation
Releases: iliaal/nameparser
Releases · iliaal/nameparser
Release list
1.7.0
Changed
- Confidence assessment uses the parser's configured honorific connectors, so unattributed German or custom connectors no longer hide title/name ambiguity. Parsed name fields are unchanged.
- Clarify whitespace normalization and the retained interior-tab exception for an empty whitespace set in the README. Keep the tab exception in one place without repeating the older caveat.
- Clarify in the README that invalid UTF-8 is scrubbed with the process-wide
mb_substitute_character()setting, which the parser does not change, and that a customsetMappers()list still applies to unsplit no-comma input undersetSurnameFirst(true)(Kim,Dr. Kim). Parsing behavior is unchanged.
Fixed
- Explicit dot-separated initials no longer become surname particles in mixed-case names:
John D.E. Smithkeeps initialsD Eand surnameSmith. Undotted particles, uniform-uppercase ambiguity handling, and configured combined-initial limits are unchanged. - Nickname-delimiter sanitization no longer copies every valid configured pair before keeping the first 32. Large configurations use bounded temporary storage while retaining validation of all entries.
- Surname particles remain attached after given-name initials:
J. van Goghnow keeps first nameJ.and surnamevan Gogh, rather than promotingvanto the first name. This also applies with a salutation, nickname, or trailing credential. See the parser regression cases. - Numeric symmetric nickname delimiters now obey token boundaries when splitting names and shield nickname commas during standalone confidence assessment.
- The input byte limit is checked before UTF-8 scrubbing as well as after it, so malformed input cannot bypass the limit when replacement removes or shortens invalid sequences.
1.6.0
Security
- Long runs of single-letter roman suffix tokens no longer cost quadratic CPU. A comma, salutation, comma-credential, or surname-first name followed by many
I,V, orXtokens (Smith, V V V ...) rescanned every earlier token for each candidate, so a 16 KB row took about 20 s of CPU and a row near the 1 MiB limit could take many minutes. The suffix scan now finds the preceding name token once per pass, and a 16 KB row takes about 0.03 s. Runs of parenthesized(MA)credentials were also quadratic, at a lower constant (about 1.5 s for 16 KB), and are fixed too. Earlier notes that described hostile-input parsing as linear did not hold for these shapes. setWhitespace()now throwsInvalidArgumentExceptionfor a whitespace set that is not valid UTF-8, instead of falling back to byte-level collapsing. With such a set, such as" \xA0"written for a no-break space, collapsing split multibyte characters and control stripping was skipped, so escape, bell, and bidi override characters and invalid UTF-8 reached name fields and exports.parse(),Confidence::assess(), and theNameconstructor apply the same check, and control stripping now always runs on scrubbed input, so output is always valid UTF-8.- Normalization strips every Unicode format character (category Cf), not only a fixed list of bidi and zero-width characters, so the soft hyphen, tag characters (which can carry hidden text), and interlinear annotation marks no longer reach name fields. The line and paragraph separators U+2028 and U+2029 become spaces instead of passing through inside a token. Variation selectors are kept.
- An empty whitespace set (
setWhitespace('')) no longer skips control stripping. C0, C1, and bidi control characters are removed as with any other set; tab is kept in the text.
Fixed
- With many custom nickname delimiter pairs, a row near the 65,536-token limit no longer exhausts memory. The suffix scan kept one array slot per pair per token (about 75 MB peak for 32 pairs); it now keeps compact state only for pairs that open a span, so the same row peaks at about 19 MB and fits a 64 MB
memory_limit. Confidencecounts names the way the parse maps them, so a name-colliding title followed by one surname is flagged even when a single-letter suffix follows (Lord Ashcroft V), the title is wrapped ("Lord" Ashcroft,(Lord) Ashcroft), another name-colliding title follows (Lord Pastor Smith), or a stray connector trails (Lord Ashcroft and).Lord Junioris flagged for the same reason, while"Lord", Smithstill readsLordas the surname and stays unflagged. An honorific run that joins a connector or an unambiguous title (Lord and Lady Smith,Lord Rev. Smith) shows the lead is a title and is not flagged.- An internal apostrophe no longer hides a trailing credential:
John D'Angelo MD'keepsD'Angeloas the last name andMDas the suffix. - A single-letter roman token after a given name in a comma segment stays an initial:
Smith, Michelle, Igives first nameMichelleand initialI. - Replacement nickname delimiters keep literal quotes in the nickname: with
setNicknameDelimiters(['[' => ']']),John ['Twas] Smithgives nickname'Twas. Name::getConfidence()tokenizes the way the parser does, so NUL whitespace and nickname-shielded commas no longer split or over-count tokens, and a parsed row with many shielded commas no longer throwsLengthException.- Nicknames between surname particles no longer invent a given name:
van (Bob) der Bergkeeps the complete surnamevan der Berg. - Combined initials no longer split digits, punctuation, or caseless characters out of a token, so
Smith, A1 JohnkeepsA1as the first name. - Confidence tokenization, delimiter handling, and comma credential tails scan each token a bounded number of times instead of rescanning earlier tokens.
- Nickname delimiters made of Unicode format characters are ignored like whitespace and control delimiters, since normalization strips those characters and the pair could never match.
1.4.1
Fixed
- A comma tail of unrecognized credentials no longer swallows the whole name. Since 1.3.0,
Christina Nemec, LMHPread the tail as the given name and the rest as the surname, reporting first nameLmhpand last nameChristina Nemec; the credential dictionary was the only thing standing between a correct split and a wrecked one, soJohn Smith, MDwas fine whileJohn Smith, XYZwas not. A tail whose tokens are all upper case, with at least one outside the dictionary, is now read as a credential run whenever the left side already carries a given name of its own. Spaced and numbered remainders ride along with it (Lori Shelley, PHARM D,Leon Ellerb, OTA/L 2838). First and last accuracy over 30,000 NPPES rows rises from 95.94% to 97.18%. - The surname-only comma form is unchanged, so
Smith, JOHN, MDstill keepsJohnas the given name,Hidalgo Castillo, Mariastill keeps the compound surname, andNguyen, VIstill readsVIthe way the dictionary says. Uniform-case input carries no casing signal and is left alone, and a tail of nothing but single letters or digits never promotes on its own (Samuel Assam, P).
1.4.0
Added
- Joint salutations parse as one honorific, so
Mr. and Mrs. Brad SmithkeepsBradas the first name instead of reading the connector as one (Mr. & Mrs.normalizes to the same value). Any pairing works (Dr. and Mrs.,Ms. & Ms.), and the connector needs a title on both sides, soMr. and Brad Smithis left alone. Fixes #4. Name::isJoint()reports a name whose honorific covers two people, for callers importing households. The parsed given and family name belong to the person actually named. Only the title-anchored form is detected: a bareBrad and Jane Smithhas no honorific to anchor the connector and reportsfalse.Name::getSalutations()returns the honorific split one entry per person addressed, so a contact record with a single prefix field can take the first:Mr. and Mrs. Brad Smithgives['Mr.', 'Mrs.'], and the partner is that second title plusgetLastname(). Stacked titles for one person stay in one entry (Rev. Dr John Doegives['Rev. Dr.']), and a name with no honorific gives an empty list.Name::getPartner()returns the second person a joint honorific addresses as aNameof her own, so a household import can build the partner contact without assembling strings:Mr. and Mrs. Brad Smithgives a partner with salutationMrs.and lastnameSmith. A particle surname crosses over whole (van der Berg); the given name, initials and any credential stay with the person actually named. Returnsnullwhen the honorific covers one person.- Irish surname particles
Ó,Ní,Nic,Uí,Ua, andMhic, soÉamon Ó CuívkeepsÓ Cuívas the surname instead of readingÓas a middle initial. They render capitalised, unlike the continental particles, and the fada survives uniform-caps input (ÉAMON Ó CUÍV→Ó Cuív). Only the fada-bearingÓcounts: anglicisedEamon O Cuivis indistinguishable from a middle initial (John F Kennedy), so bareOstays an initial. - English honorifics
Dame,Lady,Lord,Pastor,Professor,Reverend, andRt Hon(Lord Ashcroft→ salutationLord, surnameAshcroft).Rt Honalso matches its abbreviated and article-led forms (Rt. Hon. Boris Johnson,The Rt Hon Boris Johnson).
Changed
Confidence::assess()flags a two-token input led by an honorific that is also a real name (Lord Ashcroft,Pastor Gonzalez,Hon Chan), where the title reading leaves no given name and nothing in the input decides between the two. A comma resolves it structurally (Lord, Jack) and a third token leaves a given name either way (Lady Diana Spencer), so neither is flagged.parse()and standaloneConfidence::assess()now reject inputs over 1,048,576 bytes or 65,536 non-empty tokens withLengthExceptionbefore structural allocation.setMaxCombinedInitials()now accepts only 0 through 64 and combined-token expansion is capped at 131,072 output parts per parse, preventing hostile configuration from creating an unbounded part list. Nickname delimiter configuration is similarly bounded to 32 pairs of at most 64 bytes each.- Large runs of combined initials, repeated salutations, and surname-first honorifics now parse in linear time instead of repeatedly reindexing the token array.
- Comma-heavy malformed input no longer retains duplicate segment projections, cutting peak working memory for a 1 MB row from hundreds of megabytes to a linear bound.
- Maximum-size interior tokens no longer materialize an array containing every grapheme, so a valid 1 MB row stays within a 32 MB PHP memory limit.
- Camelcase detection remains linear without PCRE JIT at the 1,024-byte boundary, and maximum-size common-prefix nickname-delimiter configurations no longer multiply the structural-comma scan.
- A pure all-caps unknown-candidate segment with no prior dictionary anchor is kept as a name rather than promoted to a suffix when a later credential appears (
Smith, JOHN, MD→ firstJohn, suffixMD). Unknown candidates still ride after a known credential (MD, FACS) and peel from a mixed segment onto a later dictionary segment (John FACS, MD). PreferSmith, MD, FACSwhen the unknown stands alone before the known credential. getConfidence()now uses the parser's configured suffixes, salutations, and token boundaries. StandaloneConfidence::assess($string)retains English defaults.
Fixed
- A conjunction the honorific could not absorb is no longer title-cased into a name part, so
Andrew and Sally Smithreports the middle nameSallyinstead ofAnd Sally, andMr. and Brad Smithreports the first nameBradinstead ofAnd. A title directly after such a conjunction addresses a second person, so it is dropped from the getters too (Mr. Andrew and Mrs Sally Smithno longer carriesMrsas a middle name). Both are marked asPart\Ignored, which no getter exports, so the text is still reachable throughName::getParts(). The parser still does not identify the second person:isJoint()andgetPartner()need a title on both sides of the conjunction, and the second given name stays where it lands. - Every word of an unattributed multi-word title is ignored after a conjunction, so
Mr. Andrew and His Honour Sally Smithkeeps onlySallyas the middle name. - A leading credential in a comma given-name segment no longer authorizes a later uppercase given name as an unknown credential (
Smith, MD John PAULkeeps middle namePauland suffixMD). Name::isJoint()now requires two non-empty salutation groups rather than reporting true for a manually constructed orphan connector.- Invalid-UTF-8 confidence input now enforces the same 65,536-token ceiling before returning its ambiguity note.
- A two-letter surname particle written in caps inside mixed-case input is no longer shredded into initials, so
Jean DE Vrieskeepsde VriesandMary LE Blanckeepsle Blancinstead of reporting initialsD EandL Ewith the particle dropped. Three-letter particles were never affected (VONexceeds the combined-initial limit). - Decomposed Unicode accents stay attached to their base letter during casing, initial splitting, and short-surname detection.
- Credentials before a trailing nickname are retained in both Western and comma forms.
- German-only and other custom language dictionaries no longer throw when an English ambiguous suffix key is absent.
- Comma-separated credential tails now drop punctuation and
Unknownplaceholders under the same rules as space-separated tails. - Nickname delimiters ignored by the 32-pair and 64-byte limits no longer shield structural commas.
- Every stock mapper now accepts sparse integer-keyed input under its public array contract.
- Custom
SuffixMappersubclasses can override the protected uppercase check for ambiguous credentials again. - Canonical mixed-case credentials such as
LAcandL.Ac.are recognized without making the title-case nameLacambiguous. - Comma parsing once again dispatches through the protected
parseSplitName()extension hook. - Caller-owned mapper objects retain their dictionaries and state after parser configuration changes, and sparse custom mapper output is normalized before the next mapper runs.
- Invalid UTF-8 is reported as ambiguous by
Confidence::assess(), and repeated ambiguity reasons are emitted once. - Pure lastname export includes consumer subclasses of
Lastnamewithout foldingLastnamePrefixinto the value. - Stacked and joint salutations use the same named-person boundary in comma and surname-first forms; nickname- or credential-only tails do not satisfy that boundary.
- A salutation that is also a real name no longer swallows the comma form's surname segment, so
Lord, JackkeepsLordas the surname (previously it became a salutation and the surname was dropped entirely). This also covers the pre-existingMaster, JohnandHon, John. An unambiguous title is unchanged:Dr., Johnstill reads as a salutation. - A dictionary salutation that follows a real name token stays part of the name rather than being promoted and dropped from the getters, so
John Lord Smith JrkeepsLordas the middle name andBlair, Kathleen MASTER OF SOCIAL WORno longer reports aMr.salutation. Only a leading article may sit before an honorific (The Rev. Mark Williams); an explicitsetMaxSalutationIndex()still scans past leading name tokens. - Comma credentials retain source order, and an unknown all-caps candidate cannot cross a preserved name segment to consume a given name.
- Custom unclosed nickname delimiters are removed exactly without
ltrim()character-mask warnings, and nested delimiters use stack semantics so a mismatched closer cannot terminate an outer nickname span. - Comma and surname-first subparsers honor custom whitespace, and parsed confidence uses the parser's configured suffix dictionaries.
- Segment sub-parsers inherit nickname delimiters, so a custom multi-character pair shields commas on the left segment when a structural comma follows (
John %%Bob, Jr%% Smith, MD). - Surname-side nicknames are extracted when the given segment is a real name (
John (Bob) Smith, Jane). setWhitespace('')no longer leaves empty tokens that pollutegetGivenName()/full_namewith a stray space on double-spaced input.- Empty nickname closer strings are dropped with empty openers, so invalid delimiter config degrades to a no-op instead of an unclosable span.
- Uniform-uppercase detection on comma input splits on commas as well as whitespace, so comma-dense rows do not force a full-string Unicode letters scan.
- Comma-separated non-empty segments count toward the parser's 65,536-token ceiling.
- Credential-only comma tails no longer reinterpret given names such as
DellaandVanas surname particles. - A quoted nickname whose closing quote immediately precedes a structural comma stays intact.
- Connector-heavy salutation input is validated in linear time, and title-only connector chains no longe...
v1.3.0
Added
- Dutch honorifics (Dhr., Mevr., Mw.) in the default parser, so "Dhr. Jan de Vries" reads the title as a salutation instead of a first name.
- Legal credential JD, so "King, Michelle JD, LPC" keeps both credentials in the suffix.
Name::getSource()returns the normalized input the name was parsed from (null for a manually constructed Name), the same stringgetConfidence()assesses.
Changed
Name::toArray()['given_name']is now documented as first, middle, and initials only. Usefull_namewhen you need the given name plus surname.- Custom mapper lists set with
setMappers()now survivesetMaxCombinedInitials(),setMaxSalutationIndex(), andsetNicknameDelimiters(). Passing an empty list resets the parser to the default pipeline. - Hostile inputs (kilobytes of unmatched quotes, 100 KB tokens of any case shape, megabyte rows) now parse in linear time and bounded memory.
LanguageInterfacedocuments the dictionary key format: keys must already be normalized (lowercase, periods removed, no edge punctuation) asText::key()produces, and may be int or string, so a numeric ordinal like the German "2." keys under the bare digit.- Export is faster on large batches; a full parse-plus-
toArray()row is net faster than 1.2.0, while rawparse()alone is slightly slower from the new credential and comma safeguards.
Fixed
- Credential-only rows such as "Jane DDS" and initial-plus-credential rows such as "John A. MD" now keep the credential in the suffix instead of rewriting it as the last name.
- All-caps short given names stay intact beside mixed-case salutations or credentials, so "JO ANDERSON PhD" keeps first name Jo.
- Parenthetical credentials such as "Jane Doe (MD)" now parse as suffixes instead of nicknames.
- Comma surname segments with real given names keep non-particle compound surnames and left-side suffixes, so "Hidalgo Castillo, Maria" and "Doe Jr, John" parse as expected.
- Interrupted credential tails keep recognized credentials in the suffix while preserving name-like bridge tokens; placeholder/punctuation noise such as "Unknown" or "-" is stripped from the tail, including immediately before the first credential ("Jane Doe Unknown MD").
- Bare single-letter roman numerals stay part of the name instead of becoming a suffix, so "Malcolm X" keeps X as the last name. Multi-letter forms ("John III") still parse as suffixes.
- A trailing comma with nothing after it ("John Smith MD,") no longer appends a trailing space to the first name.
- Nicknames preserve internal apostrophes, so "John (O'Brien) Smith" keeps O'Brien.
- Single-token salutations such as "Mr" now parse as salutations under the default salutation scan.
- Trailing punctuation no longer blocks credential lookup for tokens such as "MD;" and "MD)".
- Unknown trailing credentials are kept when a known credential anchors the tail, so "John Smith MD FACS" keeps both MD and FACS in the suffix instead of leaking FACS into the name. Uniform all-caps rows still cannot recover an unknown credential (casing carries no signal there).
- A credential-only segment after the given name is pulled out to the suffix, so "Smith, MD, John" keeps first name John and credential MD instead of reading MD as a name. Mixed given-plus-credential tails ("John Smith, MD, FACS") keep all credentials.
- A leading credential run in the given segment maps to the suffix, so "Smith, MD John" keeps first name John and credential MD instead of shredding MD into initials.
- The confidence pass keys punctuation-wrapped tokens the same way the parser does, so "NGUYEN, VI;" is flagged as ambiguous instead of slipping past on the trailing semicolon.
- German ordinal suffixes are recognized, so "Friedrich Wilhelm 2." keeps 2. as the suffix (the ordinal keys under the bare digit).
- Caseless-script names are not split into initials, so "Wang, 李明" keeps 李明 as the first name instead of splitting it into two initials.
- Comma-form initials use the whole input's casing, so "Smith, JM" splits JM into J and initial M (the mixed-case surname proves the signal) while all-caps "SMITH, JM" keeps Jm as a first name.
- Surname-first parsing handles a leading salutation and a credential-only tail, so "Dr. Kim Jong Un" keeps surname Kim (not the title) and "Kim Jong Un, MD" keeps surname Kim with credential MD instead of falling back to Western order.
- A comma inside a delimited nickname no longer bisects the name and survives into the nickname, for bracketed, quoted, and custom multi-character forms alike: "John (Bob, Jr) Doe", "John 'Bob, Jr' Doe", and "John <<Bob, Jr>> Doe" (with
['<<' => '>>']) keep nickname "Bob, Jr", and the given-side "Smith, John (Jack, Robert)" keeps "Jack, Robert" whole. - An all-caps token behind a preserved name token stays combined initials, so "John Paul JM Smith MD" keeps initials J M; an unknown credential is only recognized inside the contiguous run at the tail.
- Surname-first input with both a leading salutation and a credential-only comma tail keeps the surname, so "Dr. Kim Jong Un, MD" gives surname Kim, salutation Dr., and credential MD.
- Multibyte custom whitespace (U+3000, NBSP) no longer corrupts unrelated glyphs that share its bytes; the collapse pattern matches whole characters. A whitespace set that is not valid UTF-8 keeps the old bytewise semantics instead of warning on every parse.
- Invalid-UTF-8 nickname delimiter keys are ignored instead of emitting a compile warning per token.
- Nickname delimiters accept multibyte and multi-character opener/closer pairs, and empty-string delimiter keys are ignored instead of emitting a warning per parse.
- An elided surname particle survives instead of being read as an unterminated nickname or an initial, so "'t Hooft" keeps the leading particle.
- Spaced parentheses yield a clean nickname, so "John ( Bob ) Smith" gives nickname Bob without stray spaces, and a delimiter pair that cleans to nothing emits no nickname at all.
- A name part that normalizes to "0" survives
getAll()and the string cast, so "Jane 0" is not silently dropped. setWhitespace('')no longer emits a warning per parse; an empty whitespace set simply skips the collapse step.
v1.2.0
Added
- Dutch and Spanish surname particles (den, ten, los, las), so "van den Heuvel" and "de los Santos" keep the full surname.
- German (vom, zu, zum, zur) and French (le, des) particles in the default parser, so "vom Bruch" and "le Pen" parse without a language class.
- Portuguese (do, dos, das), Filipino joined (dela, delos, delas), and Italian (lo) surname particles, so "Joao dos Santos", "Maria dela Cruz", and "lo Russo" keep the full surname instead of orphaning the particle into the middle name.
setSurnameFirst(true)reads comma-less names in CJK order (surname first), so "Mao Zedong" gives last "Mao". Opt-in; auto-detection from romanized text is not possible.
Fixed
- Comma form "Last, First" no longer leaks a leading surname particle into the first name ("van der Berg, Johan" gives last "van der Berg", not first "Van Johan").
- A multi-particle surname with no first name keeps its leading particle ("von der Heide", "Dr. de la Cruz"), instead of reading it as the first name.
- A particle in a compound given name renders lowercase, matching surname prefixes ("Maria del Carmen" gives middle "del Carmen").
v1.1.0
Added
Name::toArray()returns every part under a fixed key set (empty string when absent), a machine-readable shape that is safe to consume without existence checks, unlikegetAll().Name::getConfidence()exposes the advisory confidence signal on the parsed result, derived from the same input the parser saw.Parser::parse()output is unchanged; the check is opt-in.- Confidence now flags all-caps tokens that collide with Census surnames (II, III, IV, MBA) in uniform-case input, in addition to the existing name-leaning keys.
- Two-letter given names in all-caps input are kept as names instead of being split into initials; "JO ANDERSON" keeps first name Jo. Mixed-case combined initials like "JM Walker" still split.
- Comma input keeps a middle name after a second comma; "Smith, John, Robert" keeps Robert, while trailing and credential-only segments like "Smith, MD, PhD" still strip to suffixes.
Changed
- Config setters (
setMaxCombinedInitials,setMaxSalutationIndex,setNicknameDelimiters) take effect on a reused parser even when called after the firstparse(), instead of using configuration cached on that first call. getFullName()andtoArray()['full_name']no longer pad with a stray space when the first or last name is absent; "John" alone returns "John", not "John ".
Fixed
- A lone bracket or quote token no longer crashes
parse()with a TypeError; inputs like "(" or "Smith, (" return an empty Name instead of aborting the row. - Multi-word salutation matching no longer accepts a partial tail, so "Smith, Her" keeps Her as the given name instead of reading it as "Her Honour", and no longer reads past the token list when a match shrinks it.
v1.0.0
Added
- Casing-aware credential matching: an ALL-CAPS token reads as a credential, title or lower case as a name, so surnames like Do, Vi, Ma, and Ba no longer parse as suffixes.
- Nursing and allied-health credentials from the NPI registry (RN, NP, PharmD, APRN, PA-C, OTR/L, and 30+ more); first/last accuracy on 30k real names rose from 92.8% to 95.3%.
Confidence::assess()flags names whose credential-vs-name split is undecidable from casing, for manual review.- Expanded base credential and salutation dictionary (DDS, DO, DVM, PsyD, LCSW, Hon., roman numerals VI to X), from the CodeByZach fork.
Changed
- Namespace is
Iliaal\NameParser(wasTheIconic\NameParser). - Requires PHP 8.3+ and
ext-mbstring. Tested through PHP 8.5. - Tooling: PHPUnit 12, PHPStan 2 (level 9), PHP-CS-Fixer, GitHub Actions.
Fixed
- Unclosed nickname delimiter no longer swallows the surname or leaks a stray bracket; "John (Bob Smith" keeps last name Smith (via tobyberster/name-parser).
- Multibyte initials are no longer corrupted; accented tokens like "É Durand" survive instead of becoming replacement characters.
- Trailing comma-separated credentials are no longer dropped; "Smith, John, MD, PhD" keeps both.
- Empty nickname no longer renders as "()" in the string cast of a name.
setWhitespace()now trims the configured characters from the edges of the input.setMaxSalutationIndex()larger than the token count no longer emits undefined-array-key warnings.