diff --git a/docs/design/rules/P9-particle-that-is-also-a-suffix-word-beside-a-family-comma.md b/docs/design/rules/P9-particle-that-is-also-a-suffix-word-beside-a-family-comma.md index a16c0d4cf..52512de41 100644 --- a/docs/design/rules/P9-particle-that-is-also-a-suffix-word-beside-a-family-comma.md +++ b/docs/design/rules/P9-particle-that-is-also-a-suffix-word-beside-a-family-comma.md @@ -10,8 +10,8 @@ ``` - **A lone letter behind it reads as it does in the same part without the particle**: a capital is a name word ('SMITH VD V, JOHN' as 'SMITH V, JOHN'), a lowercase letter in a name of mixed case the numeral ('Smith vd v, John' as 'Smith v, John'), and a name written in one case reads as its capitals (T4). ``` - "SMITH VD V, JOHN" → family="SMITH VD V" deviates: #1080 (today: family="SMITH VD") - "smith vd v, john" → family="smith vd v" deviates: #1080 (today: family="smith vd") + "SMITH VD V, JOHN" → family="SMITH VD V" + "smith vd v, john" → family="smith vd v" ``` - **In the part after a family comma**, where P6 attaches a particle ending the name to the family, a lone word of both the particle and the UNAMBIGUOUS suffix vocabulary standing INSIDE a run read as post-nominals — one in front of it and another behind, a credential or a generation, whatever made each one, and a title between it and the one in front transparent (H5) — is not the tussenvoegsel: that stands right behind the given name, so a post-nominal between them says the word is not one. It does not end the name, and keeps the post-nominal reading in any case, reported as S4's credential fork. ``` diff --git a/docs/release_log.rst b/docs/release_log.rst index 0f0383fd4..8eda4712b 100644 --- a/docs/release_log.rst +++ b/docs/release_log.rst @@ -75,6 +75,7 @@ Release Log - **Fix the surname Mba read as the MBA degree.** ``parse("Chinedu Mba")`` gives family ``Mba``, as 1.4.0 did, where 2.0.0 through 2.8.1 gave suffix ``Mba`` and no family. ``John Smith, MBA`` and ``John Smith MBA`` still read the degree; where a name word can stand, it reads as the name, as ``MA`` does (``Smith, MBA Jane`` gives given ``MBA``), and brackets configured for maiden names read ``(MBA)`` as the maiden name. :issue:`1005` - **Fix the Filipino particle Delos, de los written as one word, reading as a middle name.** ``parse("Juan Delos Reyes")`` gives family ``Delos Reyes``, as ``Juan Dela Cruz`` gives ``Dela Cruz``, where 1.4.0 through 2.8.1 gave middle ``Delos``, family ``Reyes``. ``Delos`` is a given name too, so ``Delos Wheeler`` still gives given ``Delos``, now reporting particle-or-given. Case repair keeps both capitalized: ``parse("JUAN DELA CRUZ").capitalized()`` gives ``Juan Dela Cruz``, where 1.4.0 through 2.8.1 gave ``Juan dela Cruz``. :issue:`1037` - **Fix a family-comma name losing the given name Junior to the suffix.** ``parse("Silva, Junior Carlos")`` gives given ``Junior``, middle ``Carlos``, as 2.2.0 and 2.3.0 did, where 2.4.0 through 2.8.1 gave suffix ``Junior Carlos`` and no given name. Junior and Júnior are given names too, so a name with no word to spare keeps one as the name (``Kofi Junior`` gives family ``Junior``), while ``John Smith Junior`` still gives suffix ``Junior``. A generation word you give a name reading with ``Lexicon.add`` is weighed by the count alone, as ``Junior`` is; add it with ``title_case_is_name=True`` to have Title case read as the name, as ``fils`` does. :issue:`1039` + - **Read a lone letter behind a particle before a family comma as the comma-less name reads it.** ``parse("SMITH VD V, JOHN")`` gives family ``SMITH VD V``, where 2.7.0 and 2.8.0 gave family ``SMITH VD`` and suffix ``V``: a particle is no word to spare, so the letter counts the name words in front of it alone. ``SMITH VD VI, JOHN`` keeps suffix ``VI`` and ``SMITH VD, JOHN`` suffix ``VD``. :issue:`1080` * 2.8.2 - November 4, 2026 diff --git a/nameparser/_pipeline/_assign.py b/nameparser/_pipeline/_assign.py index 25653a58a..d292cff06 100644 --- a/nameparser/_pipeline/_assign.py +++ b/nameparser/_pipeline/_assign.py @@ -778,8 +778,8 @@ def as_a_name(i: int) -> PendingReading: def _family_part_numeral(word: WorkToken, preceding: str, names: int, one_case: bool | None) -> Role | None: """The role of a roman numeral ending the family part before a - family comma, behind `names` pieces of that part, as the comma-less - name reads one ending it (rules/S10), or None where the numeral's + family comma, behind `names` name words of that part, as the + comma-less name reads one ending it (rules/S10), or None where the numeral's predicate reads no numeral there (an initial in front, a letter with its period): the suffix where it reads as the generation, a one-letter numeral needing two name words in front of it, and the @@ -1939,9 +1939,15 @@ def as_a_middle(m: int, token: WorkToken) -> PendingReading: and all(is_suffix_piece(fam_pieces[j], fam_tags[j], tokens) for j in range(k + 1, len(fam_pieces))) + # rules/P9, #1080: the numeral is read by the name + # words in front of it, a particle none to spare + # ('SMITH VD V, JOHN' as 'SMITH V, JOHN' reads) and (numeral := _family_part_numeral( word, tokens[fam_pieces[k - 1][0]].text, - k - lead, state.one_case)) is not None): + 1 + sum(1 for j in range(lead + 1, k) + if "particle" + not in tokens[fam_pieces[j][0]].tags), + state.one_case)) is not None): _set_roles(tokens, piece, numeral) # reported where taken: a name word here is the family # name the comma has just named, which nobody misreads diff --git a/tests/v2/cases.py b/tests/v2/cases.py index 362ae3953..3ff8a7919 100644 --- a/tests/v2/cases.py +++ b/tests/v2/cases.py @@ -2534,6 +2534,30 @@ def _check_cjk_shape_purity(self) -> None: "mixed-case row above does. 2.2.0 and 2.3.0 read family " "'SMITH MA', suffix 'VD'; 2.0.0 and 2.1.0 read as here; " "1.4.0 last 'SMITH', suffix 'VD, MA'"), + Case("a_lone_letter_behind_a_particle_before_a_comma_is_the_name_word", + "SMITH VD V, JOHN", + {"given": "JOHN", "family": "SMITH VD V"}, + classification="fix(#1080)", + notes="P9: a lone letter behind a particle reads as it does in " + "the same part without one, so the particle is no word to " + "spare and 'V' is a name word. 2.7.0 and 2.8.0 read family " + "'SMITH VD', suffix 'V'; 2.6.0 read as here"), + Case("a_lone_letter_behind_a_plain_particle_before_a_comma_is_the_name_word", + "SMITH VAN V, JOHN", + {"given": "JOHN", "family": "SMITH VAN V"}, + classification="fix(#1080)", + notes="#1080's table: the count leaves out every particle, not " + "only the word that is suffix vocabulary too, so 'VAN' is " + "no word to spare. 2.7.0 and 2.8.0 read family 'SMITH " + "VAN', suffix 'V'; 2.6.0 read as here"), + Case("a_lone_letter_behind_a_particle_run_before_a_comma_is_the_name_word", + "Smith van der V, John", + {"given": "John", "family": "Smith van der V"}, + classification="fix(#1080)", + notes="#1080's table: a run is no word to spare either -- its " + "length is why the count drops the whole run and not one " + "particle. 2.7.0 and 2.8.0 read family 'Smith van der', " + "suffix 'V'; 2.6.0 read as here"), Case("a_comma_before_no_name_word_keeps_the_positional_read", "Berg de MA, Prof.", {"title": "Prof.", "given": "Berg", "family": "de", diff --git a/tools/differential/snapshot.jsonl b/tools/differential/snapshot.jsonl index edcca0cbc..dcd52f728 100644 --- a/tools/differential/snapshot.jsonl +++ b/tools/differential/snapshot.jsonl @@ -3324,7 +3324,7 @@ {"name": "SEÁN Ó MURCHÚ", "given": "SEÁN", "family": "Ó MURCHÚ", "initials": "S. M.", "capitalized": "Seán Ó Murchú"} {"name": "SIG.RA MARIA ROSSI", "title": "SIG.RA", "given": "MARIA", "family": "ROSSI", "initials": "M. R.", "capitalized": "Sig.ra Maria Rossi"} {"name": "SMITH VD MA, JOHN", "given": "JOHN", "family": "SMITH VD MA", "initials": "J. S. M.", "capitalized": "John Smith vd Ma"} -{"name": "SMITH VD V, JOHN", "given": "JOHN", "family": "SMITH VD", "suffix": "V", "ambiguities": ["SUFFIX_OR_NAME"], "alternatives": ["SUFFIX_OR_NAME: initial as family (shape)"], "initials": "J. S.", "capitalized": "John Smith vd V", "generation": "V"} +{"name": "SMITH VD V, JOHN", "given": "JOHN", "family": "SMITH VD V", "initials": "J. S. V.", "capitalized": "John Smith vd V"} {"name": "SMITH VD, JOHN", "given": "JOHN", "family": "SMITH", "suffix": "VD", "initials": "J. S.", "capitalized": "John Smith VD"} {"name": "SMITH, DEAN MR.", "title": "MR.", "given": "DEAN", "family": "SMITH", "initials": "D. S.", "capitalized": "Mr. Dean Smith"} {"name": "SMITH, JOHN B. MAJ.", "title": "MAJ.", "given": "JOHN", "middle": "B.", "family": "SMITH", "initials": "J. B. S.", "capitalized": "Maj. John B. Smith"} @@ -4358,7 +4358,7 @@ {"name": "scott e. werner", "given": "scott", "middle": "e.", "family": "werner", "initials": "s. e. w.", "capitalized": "Scott E. Werner"} {"name": "scott l. werner", "given": "scott", "middle": "l.", "family": "werner", "initials": "s. l. w.", "capitalized": "Scott L. Werner"} {"name": "señora María García", "title": "señora", "given": "María", "family": "García", "initials": "M. G.", "capitalized_force": "Señora María García"} -{"name": "smith vd v, john", "given": "john", "family": "smith vd", "suffix": "v", "ambiguities": ["SUFFIX_OR_NAME"], "alternatives": ["SUFFIX_OR_NAME: initial as family (shape)"], "initials": "j. s.", "capitalized": "John Smith vd V", "generation": "v"} +{"name": "smith vd v, john", "given": "john", "family": "smith vd v", "initials": "j. s. v.", "capitalized": "John Smith vd V"} {"name": "smith, de ma", "family": "smith de ma", "ambiguities": ["SUFFIX_OR_NAME"], "alternatives": ["SUFFIX_OR_NAME: credential as suffix (vocabulary)"], "initials": "s. m.", "capitalized": "Smith de Ma"} {"name": "smith, j y lopez", "given": "j", "middle": "y lopez", "family": "smith", "initials": "j. l. s.", "capitalized": "J y Lopez Smith"} {"name": "smith, john e phd", "given": "john", "middle": "e", "family": "smith", "suffix": "phd", "initials": "j. e. s.", "capitalized": "John E Smith PhD"}