Skip to content

Commit 8147ac6

Browse files
derek73claude
andcommitted
An ambiguous acronym is a suffix when the name can spare it
v1 reads "John Smith MA" as suffix='MA' and "Jack MA" as last='MA'. That is not inconsistency — it is evidence-weighing. With only two pieces, "one of them is a credential" is the less likely reading, so the ambiguous acronym stays the surname; with three, peeling it still leaves a given and a family name, so the credential reading wins. 2.0 had generalized the period gate over every position, making a bare ambiguous acronym never a suffix, so "John Smith MA" came out family='MA', middle='Smith'. That was an unclassified divergence, and the wrong call: MA after a full name really is more likely a degree. Root cause is the same shape as the rest of this session's bugs. In v1 suffix_acronyms_ambiguous has exactly ONE use site -- parse_nicknames' delimited-content escape, deciding whether "(JD)" is a nickname or a suffix. It is a delimiter-path disambiguator, not a vocabulary property, and 2.0 promoted it to a universal rule. Narrow it to what it can decide: peel a bare ambiguous acronym only when at least two name pieces remain. This is v1's reserve_last restricted to the ambiguous set -- 2.0 still peels UNambiguous suffixes when nothing is left ("Smith PhD" -> suffix, a classified fix), because there the vocabulary is not in doubt. Verified against a live 1.4.0: 9 of 10 shapes now identical, the tenth being that classified PhD fix. Four case-table rows pin both halves of the rule, and the release-log bullet now describes it instead of claiming periods are the only path. Case rows use uppercase "MA" where a suffix is expected, so the intended reading is legible without running the parser. (Case itself is not yet an input to the decision; it could reasonably become one.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 234b35f commit 8147ac6

3 files changed

Lines changed: 33 additions & 8 deletions

File tree

docs/release_log.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ Release Log
5454
- Fix a lone recognized trailing suffix with no comma being routed to ``first``/``last``: ``"Johnson PhD"`` and ``"Mr. Johnson PhD"`` now keep the suffix in ``suffix``
5555
- Fix a split ``"Ph. D."`` credential being read as two tokens; it now classifies as one suffix, replacing v1's ``fix_phd`` hook
5656
- Fold a leading never-given particle into the family name. Note that ``Lexicon.particles_ambiguous`` is the **complement** of v1's ``non_first_name_prefixes``, not a rename -- it lists the particles that *may* double as a given name, where v1 listed the ones that may not. Copying a v1 customization across without inverting it silently reverses the behavior; see :doc:`migrate`
57-
- Add ``ma`` and ``do`` to ``suffix_acronyms_ambiguous``, so ``"Jack Ma"`` and ``"Anh Do"`` keep their family names. Ambiguous acronyms count as suffixes only when written with periods; as a side effect, a parenthesized or quoted ``"(MA)"``/``"(DO)"`` now falls through to nickname parsing instead of escaping to ``suffix``
57+
- Add ``ma`` and ``do`` to ``suffix_acronyms_ambiguous``, the set of post-nominals that are also ordinary surnames. An entry there is read as a credential when the name can spare it — written with periods (``"John Smith M.A."``), or when removing it still leaves a given *and* a family name (``"John Smith MA"`` → suffix ``MA``). With only two pieces to go around, the surname reading wins instead: ``"Jack Ma"`` and ``"Anh Do"`` keep their family names. As a side effect, a parenthesized or quoted ``"(MA)"``/``"(DO)"`` now falls through to nickname parsing rather than escaping to ``suffix``, since inside delimiters the nickname reading is the plausible one
5858
- Change ``comparison_key()`` and ``matches()`` in both APIs to fold with ``str.casefold()`` where 1.4 used ``str.lower()``, so Unicode case pairs compare equal -- ``"STRASSE"`` matches ``"Straße"``, and a Greek final sigma matches its regular form. This is strictly more permissive: anything 1.4 matched still matches. Vocabulary normalization deliberately still uses ``lower()``, for v1 parity
5959
- Change the 2.0 API's default ``render()``/``str()`` spec to show every non-empty field: ``'{title} {given} "{nickname}" {middle} {family} ({maiden}) {suffix}'``. The quoted nickname round-trips exactly; the parenthesized maiden re-parses as a nickname, a deliberate choice of presentation over lossless round-trip -- use ``née {maiden}`` in a custom spec if you need it to survive a reparse. ``HumanName`` keeps v1's own ``string_format`` default, unchanged
6060
- Change delimiter-overlap precedence in the 2.0 API: a pair listed in ``Policy.maiden_delimiters`` is dropped from the effective nickname set, so ``Policy(maiden_delimiters={("(", ")")})`` alone routes parenthesized content to ``maiden``. The default nickname set is exported as ``DEFAULT_NICKNAME_DELIMITERS``. ``HumanName`` keeps v1's nickname-wins precedence, so no existing behavior changes

nameparser/_pipeline/_assign.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,19 @@ def _assign_main(seg_idx: int, state: ParseState,
136136
and "initial" not in tokens[pieces[rest[k - 2]][0]].tags):
137137
k -= 1
138138
continue
139+
# A bare ambiguous acronym ("MA", not "M.A.") is a credential
140+
# only when peeling it still leaves a given AND a family name.
141+
# With two pieces, "one of them is a credential" is the less
142+
# likely reading, so it stays the family name -- "Jack MA" is a
143+
# person, "John Smith MA" is a person with a degree. This is
144+
# v1's reserve_last narrowed to the ambiguous set: 2.0
145+
# deliberately peels UNambiguous suffixes even when nothing is
146+
# left ("Smith PhD" -> suffix, a classified fix), because there
147+
# the vocabulary is not in doubt.
148+
if (k - 1 >= 2 and len(piece) == 1
149+
and "vocab:suffix-ambiguous" in tokens[piece[0]].tags):
150+
k -= 1
151+
continue
139152
break
140153
name_pieces, suffix_pieces = rest[:k], rest[k:]
141154
if not name_pieces and suffix_pieces:

tests/v2/cases.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -85,13 +85,25 @@ def __post_init__(self) -> None:
8585
notes="v1 PREPENDED middle_list to last_list; folded tokens "
8686
"carry vocab:folded-middle and the family view orders "
8787
"them first (spans cannot reorder)"),
88-
Case("ambiguous_surname_acronyms", "Jack Ma",
89-
{"given": "Jack", "family": "Ma"},
90-
notes="'ma'/'do' joined suffix_acronyms_ambiguous: common "
91-
"surnames need periods to read as credentials (v1 "
92-
"parity restored; data change, flag for release log)"),
93-
Case("ambiguous_surname_acronym_with_suffix", "Jack Ma Jr",
94-
{"given": "Jack", "family": "Ma", "suffix": "Jr"}),
88+
Case("ambiguous_surname_acronyms", "Jack MA",
89+
{"given": "Jack", "family": "MA"},
90+
notes="'ma'/'do' joined suffix_acronyms_ambiguous: with only "
91+
"two pieces, 'one of them is a credential' is the less "
92+
"likely reading, so the ambiguous acronym stays the "
93+
"family name (v1 parity via its reserve_last)"),
94+
Case("ambiguous_surname_acronym_with_suffix", "Jack MA Jr",
95+
{"given": "Jack", "family": "MA", "suffix": "Jr"},
96+
notes="'Jr' peels first; 'MA' would then be the only piece "
97+
"left beside the given name, so it stays family"),
98+
Case("ambiguous_acronym_is_a_suffix_when_a_family_name_remains",
99+
"John Smith MA",
100+
{"given": "John", "family": "Smith", "suffix": "MA"},
101+
notes="the other half of the same rule: three pieces means "
102+
"peeling 'MA' still leaves given+family, so the "
103+
"credential reading wins (v1 parity)"),
104+
Case("ambiguous_acronym_suffix_with_middle", "John Q Smith MA",
105+
{"given": "John", "middle": "Q", "family": "Smith",
106+
"suffix": "MA"}),
95107
Case("initial_shaped_not_conjunction", "john e. smith",
96108
{"given": "john", "middle": "e.", "family": "smith"},
97109
notes="v1 is_conjunction excludes initials at classify too"),

0 commit comments

Comments
 (0)