Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AGENTS.md

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions docs/customize.rst
Original file line number Diff line number Diff line change
Expand Up @@ -228,9 +228,9 @@ Words that are also ordinary names
Three fields — ``suffix_acronyms_ambiguous``, ``particles_ambiguous``
and ``conjunctions_ambiguous`` — mark entries from ``suffix_acronyms``,
``particles`` and ``conjunctions`` that are also plausible as ordinary
name words on their own (an acronym suffix that doubles as a nickname,
a particle that doubles as a given name, a connective letter that
doubles as an initial). They don't add new vocabulary by themselves;
name words on their own (an acronym suffix that is also
borne as a name, a particle that doubles as a given name, a connective
letter that doubles as an initial). They don't add new vocabulary by themselves;
they narrow how an existing
entry is read when it appears alone. If you're not sure whether a word
you're adding is one of these ambiguous cases, weigh how often it is a
Expand Down
7 changes: 4 additions & 3 deletions docs/design/decisions.md

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions docs/design/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -1039,6 +1039,12 @@ S2. Rationale: generational suffixes and credentials are recognized
"JACK MA" → family="MA"
"JOHN SMITH MA" → suffix="MA"
"John Smith Ma" → family="Ma"
"wang meng" → family="meng" · boundary
"tran lac" → family="lac"
"john smith meng" → suffix="meng"
"john smith MEng" → family="MEng"
"Nguyen Van Lac" → family="Van Lac"
"Wang M.Eng." → family="M.Eng."
"Smith, MA" → suffix="MA"
"Smith, Ma" → given="Ma"
"Doe, John MA" → suffix="MA"
Expand Down
2 changes: 2 additions & 0 deletions docs/release_log.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ Release Log

- **Fix a credential acronym that is also a surname being read by position alone.** ``HumanName("Jack MA")`` gives suffix ``MA`` where 2.0 through 2.3 gave last ``MA``, and ``John Smith Ma`` gives last ``Ma`` where they gave suffix ``Ma``. In a name written in more than one case, an ambiguous acronym written in capitals is written the way a credential is written and is read as one even where removing it leaves no surname; one written in any other cased form that is not wholly lower is written the way a surname is written and stays one even where there are words to spare (``John Smith ma`` and ``John Smith ed`` -- all lower, no contrast -- give suffix ``ma``/``ed`` instead). A name written wholly in one case says nothing either way and keeps the reading it had: ``JOHN SMITH MA`` is still a credential, ``ANH DO`` still a surname, ``jack ma`` still a surname. The same reading reaches the comma forms, where the words-to-spare count is now a count of NAME words: ``Smith, MA`` gives last ``Smith``, suffix ``MA``; ``Smith Jr., MA`` keeps last ``Smith``; and ``John Smith, MA``, ``John Smith, Ed``, ``john smith, ma`` and ``JOHN SMITH, MA`` all give a suffix again, which is what 1.4.0 read and 2.0 through 2.3 did not. ``Jack Ma`` and ``Anh Do`` are unchanged. The LEAN is inert on a caseless script, but the comma count above is not -- it asks name-word count, not case -- so ``마틴 킹, MA`` and ``田中 太郎, MA`` also give a suffix again (1.4.0 parity on the suffix, two pre-comma name words each) while the single-token ``毛泽东, MA`` does not move, having no case to write a contrast in either way. See the ``S2`` entry of ``docs/design/decisions.md`` (closes #289)

- **Fix a bare trailing Meng or Lac being read as a credential and losing the family name: meng and lac are now acronyms that are also ordinary names.** ``HumanName("wang meng")`` gives first ``wang``, last ``meng``, and ``parse()`` reports a suffix-or-name ambiguity, where every release from 2.0.0 through 2.3.0 gave suffix ``meng`` and no last name; 1.4.0 read last ``meng``, so this is 1.4.0's answer plus the flag. ``li meng`` and ``tran lac`` move the same way, ``Wang, Meng`` gives first ``Meng``, last ``Wang`` again, and ``Parser(policy=Policy(name_order=FAMILY_FIRST)).parse("Wang Meng")`` gives given ``Meng`` where 2.0.0 through 2.3.0 gave family ``Wang``, suffix ``Meng`` and no given name. With a full name in front the credential reading stays: ``john smith meng`` and ``nguyen van lac`` keep suffix ``meng`` and ``lac``, now flagged. But a Title-case ``Nguyen Van Lac`` gives last ``Van Lac`` where every release gave last ``Van``, suffix ``Lac``. The cost is the marking's own, and it falls on the conventional spellings: a ``MEng`` or ``LAc`` written that way, in a name written in more than one case, is read the way ``John Smith Ma`` is (above), so ``John Smith MEng`` gives middle ``Smith``, last ``MEng``, where every release gave suffix ``MEng``. A credential run ending in one of them goes the same way: ``Mary Jones PhD MEng`` gives middle ``Jones PhD``, last ``MEng``, where every release read the whole run as a suffix (``PhD, MEng`` through 2.2, ``PhD MEng`` in 2.3); ``John Smith MEng PhD`` gives middle ``Smith``, last ``MEng``, suffix ``PhD`` (``rules.md#S2``'s declined pick stops the walk, the cost ``Doe, John MA Ma`` already pays). ``Wang M.Eng.`` gives last ``M.Eng.``, where 2.0 through 2.3 gave suffix ``M.Eng.``: the period gate counts a member as unambiguous only written one period per letter, and ``M.Eng.`` is chunked; ``John Smith M.Eng.`` keeps the suffix. After a comma ``Smith, MEng`` and ``Smith, meng`` give first ``MEng`` and ``meng`` (1.4.0's reading, not the suffix 2.0 through 2.3 gave) and ``Smith, John MEng`` gives middle ``MEng`` (every release gave suffix ``MEng``), and a bracketed ``John Smith (MEng)`` falls through to nickname, where every release gave suffix ``MEng``. A credential run after a comma whose last word is one of them re-reads the comma as a family comma, whatever its letter case: ``John Smith, PhD MEng`` gives first ``PhD``, middle ``MEng``, last ``John Smith`` and ``john smith, phd meng`` gives first ``phd``, last ``john smith``, suffix ``meng``, where every release read the run as a suffix; a title-listed credential in front reads as a title instead (``Jane Doe, MS LAc`` gives title ``MS``, first ``LAc``, unflagged). The path is the one ``john smith, phd ma`` already takes. A lone credential after a comma behind a full name (``John Smith, MEng``) keeps the credential reading, and so does a second comma before a run that ends in one (``John Smith, PhD, MEng`` keeps suffix ``PhD, MEng``); writing it in capitals does not (``JOHN SMITH, PHD MENG`` still re-reads the comma). Meng is a common Chinese surname and given name, Lac a Vietnamese given name (``Nguyen Van Lac``) and a French surname; see the ``suffix-acronym-collisions`` entry of ``docs/design/decisions.md`` (closes #540)

- **New Policy field unlisted_dotted_suffixes, on by default: a dotted acronym nobody has listed is read by position.** ``HumanName("John Smith X.Y.Z.")`` gives suffix ``X.Y.Z.`` where every release gave last ``X.Y.Z.``, while ``Jack X.Y.Z.`` keeps its surname, the same words-to-spare rule a listed acronym takes -- and both readings are reported. Case is irrelevant here: the periods are the signal, so ``john smith x.y.z.`` reads the same way. Words the vocabulary does know are untouched (``M.A.``, ``Ph.D.``, ``A.B.C.``), a single trailing period is still not this shape (``John Smith Xyz.`` keeps last ``Xyz.``), and a dotted run at the FRONT of a name is untouched (``J.R.R. Tolkien``). One accident retires with it: a dotted word whose only vocabulary matches were SINGLE ASCII CHARACTERS -- the roman numerals the suffix list holds, and the lone digit ``2`` -- was reading as a generational suffix, so ``Jack X.Y.I.`` gives last ``X.Y.I.`` again, as 1.4.0 read it, while ``Msc.Ed.``, ``JD.CPA`` and ``Lt.Gov.`` are unchanged. The digit is why a dotted VERSION STRING moves with them and moves SILENTLY: ``John Smith 1.4.2`` gives last ``1.4.2`` where 2.3 gave suffix ``1.4.2``, and ``John Smith, 1.4.2`` gives first ``1.4.2``, last ``John Smith``. Such a token reports nothing at any policy -- it is no acronym either, the shape reading wanting every chunk alphabetic -- and a version string read as a credential was the same accident this retirement removes. That retirement is NOT behind this switch and stands either way -- setting it to ``False`` reads an unlisted dotted word as name material by position instead (``John Smith X.Y.Z.`` keeps last ``X.Y.Z.``), the pre-2.4 reading for THAT half alone. See the ``S2`` and ``suffix-acronym-collisions`` entries of ``docs/design/decisions.md`` (closes #516)

- **New Policy field unlisted_caps_suffixes, off by default: an opt-in reading for an unlisted all-caps credential.** It reaches the core parser only -- ``Parser(policy=Policy(unlisted_caps_suffixes=True))`` -- since the field has no v1 ``Constants`` manager. With it on, ``.parse("John Smith XYZ")`` gives given ``John``, last ``Smith``, suffix ``XYZ``, and ``.parse("John Smith, XYZ")`` gives the same three fields. It is off by default because an all-caps surname is a real writing convention that shape cannot separate from a credential: ``Jean DUPONT``, ``Minjun KIM`` and ``Jean Pierre DUPONT`` are surnames in French and Korean records, and the last of those gives given ``Jean``, last ``Pierre``, suffix ``DUPONT`` with the switch on. Off, nothing changes and nothing is reported -- 1.4.0's reading for that whole class. Neither of the two new fields reaches the v1 ``Constants`` API, as ``lenient_comma_suffixes`` does not: a ``HumanName`` tracks the parser's own DEFAULTS, so the dotted reading above (default on) reaches it while this one (default off) cannot be turned on from there. See the ``S2`` entry of ``docs/design/decisions.md`` (closes #516)
Expand Down
43 changes: 28 additions & 15 deletions nameparser/config/suffixes.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,23 +211,24 @@
"""
SUFFIX_ACRONYMS_AMBIGUOUS = frozenset({
# Suffix acronyms that also commonly work as given-name nicknames on
# their own (e.g. "Ed", "JD"). Two readers in 2.x, not the single v1
# one this comment used to name: _extract._suffix_shaped, deciding
# whether parenthesized/quoted content is a nickname or a suffix
# (content matching one of these stays a nickname, the more common
# reading in ambiguous, delimiter-only context), and _vocab's
# suffix_as_written, which excludes the ambiguous subset from plain
# acronym membership so the period gate below is not dead code.
# _classify also tags membership as "vocab:suffix-ambiguous".
# their own (e.g. "Ed", "JD"). Read wherever the pipeline decides an
# acronym's bare reading (rules.md#S2) and by the delimited-content
# escape (content matching one of these stays a nickname, the more
# common reading in ambiguous, delimiter-only context). _classify
# also tags membership as "vocab:suffix-ambiguous".
#
# When adding a new entry to SUFFIX_ACRONYMS, also add it here only if
# the exact letter sequence could plausibly be someone's name on its
# own -- a given name or nickname (e.g. 'jd', 'ed') or a common
# surname (e.g. 'ma', 'do'). Unambiguous certifications/degrees
# (e.g. 'mba', 'cpa', 'phd') don't need an entry. In 2.0 this set
# also gates bare recognition: an ambiguous acronym counts as a
# suffix only when written with periods ('M.A.' yes, 'Ma' no), so
# 'Jack Ma' keeps its family name.
# also gates bare recognition: a bare ambiguous acronym reads as
# the credential only with words to spare in front of it, or
# written in capitals inside a mixed-case name; inside a
# mixed-case name any other cased form that is not wholly lower
# ('Ma', 'MEng') reads as the name even with words to spare --
# rules.md#S2. The parse reports the fork where it decides one,
# so 'Jack Ma' keeps its family name.
#
# The other half of the criterion, added 2026-09-07 with #342.
# Being borne at all is only the entry ticket; what decides among
Expand All @@ -248,7 +249,13 @@
# Where the CREDENTIAL dominates, the entry stays unambiguous.
# LENGTH is a correlate and not the test -- a short acronym is
# more often a common credential AND more often a name -- so do
# not read the letter counts here as a rule.
# not read the letter counts here as a rule. #540 (2026-09-25)
# marked 'lac' and 'meng', the first entries here longer than
# two letters: MEng and LAc are credentials people write after
# their names, and Meng and Lac are borne in the trailing slot
# by real people, and neither reading is rare enough to give the
# word to the other -- so the marking, whatever the letter
# count.
#
# Removal takes the DOTTED spelling with it too, except by
# accident: "John Smith R.A.I." still reads suffix 'R.A.I.' only
Expand Down Expand Up @@ -283,14 +290,20 @@
'do',
'ed',
'jd',
# #540: LAc, Licensed Acupuncturist; Lac is a Vietnamese given name
# (the trailing word in native order, Nguyen Van Lac) and a French
# surname
'lac',
'ma',
# #540: MEng, Master of Engineering; Meng, a Chinese surname and given name
'meng',
})
"""

Acronym suffixes from SUFFIX_ACRONYMS that also plausibly collide with a
common given-name nickname. Not a partition of SUFFIX_ACRONYMS -- a small,
standalone exception list, read by the delimited-content escape in
``_pipeline/_extract.py`` and by ``_pipeline/_vocab.py``'s period gate.
word borne as a name -- a given name, nickname or surname. Not a
partition of SUFFIX_ACRONYMS -- a small, standalone exception list, read
wherever the pipeline decides an acronym's bare reading (rules.md#S2).

"""
SUFFIX_ACRONYMS = frozenset({
Expand Down
Loading
Loading