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
5 changes: 3 additions & 2 deletions docs/customize.rst
Original file line number Diff line number Diff line change
Expand Up @@ -575,8 +575,9 @@ listed below.
- Reads an initial-shaped suffix word after a comma as a suffix:
``"John Smith, V"`` is John Smith the fifth when ``True``
(default); ``False`` reads ``V`` as a given-name initial
instead. Multi-letter suffixes (``III``, ``MD``) are
unaffected.
instead, and ``"john smith, v"`` the same way, since a name
written all in one case reads as its capitals. Multi-letter
suffixes (``III``, ``MD``) are unaffected.
* - ``unlisted_dotted_suffixes``
- ``bool``
- Reads an unlisted dotted acronym such as ``X.Y.Z.`` as a
Expand Down
6 changes: 4 additions & 2 deletions docs/design/decisions/T4-bare-letter-as-an-initial.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
- 2026-10-18, #723 — A ONE-CASE BARE LETTER IS THE INITIAL ITS CAPITAL IS. #721 applied S10's "a name written wholly in one case says nothing about any letter in it" to the one-letter roman numerals only (decisions/S10, the Open line of its /simplify entry), so a bare lowercase `j` was a name word where `J` was an initial, and every rule that reads an initial differently read the two spellings of one name differently: M4 (`j nee smith` family 'j', `J NEE SMITH` given 'J'), O5 (a lone `j` reported `given-or-family`, `J` did not), and P3's count, which passes over an initial (`jose j y lopez` family 'j y lopez', `JOSE J Y LOPEZ` middle 'J Y', family 'LOPEZ', which 1.4.0 read too). The rule is now stated once, for every letter, and the numeral case is an instance of it: in a name written in one case, a bare letter is tagged an initial wherever its capital would be. Its capital is asked of the bare-capital shape (one ASCII capital), so `é` and `ж`, whose capitals are no initial, read as before in both spellings. `ı` and `ſ`, whose capitals are `I` and `S`, are tagged initials too; `ı`'s twin is not a true one, since `I` is also the marked Catalan connective and `ı` is in no vocabulary (`JOSE I LOPEZ` reports `conjunction-or-initial`, `jose ı lopez` nothing). Measured on the first commit's tree against 16ee2851, a dated snapshot from a scratch grid not committed: letters a–z, `é` and `ж`; heads (a, b, c) = (john, smith, jones), (jose, lopez, garcia), (mary, ma, lee); all three orders; the 35 shapes `{n}`, `{n} nee {b}`, `{a} {n}`, `{n} {a}`, `{a} {n} {b}`, `{a} {n} y {b}`, `{a} {n} e {b}`, `{b}, {n}`, `{b}, {n} y {c}`, `{b}, {a} {n}`, `, {n}`, `{n}, phd`, `{a} {n}, phd`, `{a} {n}, jr.`, `{a} nee {b} {n}`, `{a} nee {n}`, `{a} {b} nee {n} {c}`, `dr {n}`, `dr {n} {b}`, `sir {n} {b}`, `{a} van {n}`, `{n} van {b}`, `{a} van {n} {b}`, `abdul {n}`, `{n} {a} {b}`, `{a} {n} {n} {b}`, `{a} {n}. {b}`, `({c}) {n} {b}`, `{n} y {b}`, `{a} {n} phd`, `{a} {b} {n} prof.`, `{n} {b} {n}`, `{n} {n}`, `{a} {n} de la {b}`, `{b} {n} jr`; each lowercase text against its `.upper()`, compared on case-folded fields, sorted report kinds and `initials()`, keyed by order and text, so the 8,820 products collapse to 7,794 distinct pairs where a shape has no head. 16ee2851 disagreed on 504 of them, this tree on none, and no capital spelling moved. The committed invariant is `test_a_one_case_name_reads_as_its_capitals` (tests/v2/test_properties.py), whose grid gains the issue's shapes and the letters `j`, `y` and `e`; its stated controls are the recompute.
WHAT MOVES: lower-case letters in a name whose own words are written in one case, each onto its capital twin's reading — wholly lower-case names, and (since the case question is asked of the own words, P3) a one-case name beside a clause of another case, so `j nee Smith` reads given 'j' as `J nee Smith` does. In three classes — a lone letter loses O5's report, a letter before a maiden marker keeps the given name M4 would have taken, and a letter beside a single-letter link is passed over by P3's count, so the link in what is then a three-word name stays a name word. No corpus name moved (the snapshot and the CLDR, GND and native-script agreement records are unchanged). And P3's Accepted paragraph on a declined marker loses its "one shape" in which the name's own words read differently in the two spellings: `dr. née juan y lopez` read family 'juan y lopez' where `DR. NÉE JUAN Y LOPEZ` reads middle 'JUAN Y', because the `y` past the declined marker is outside the own words P3's fork reads; it is this rule's letter now, and reads as its capital does.
And one move the snapshot cannot see (R4): case repair of a connective letter in a one-case maiden clause, now tagged an initial as its capital is, so `jose lopez nee y garcia` repairs to `Jose Lopez (Y Garcia)` where it gave `(y Garcia)` — the same tag in the two spellings that P3's rewritten paragraph intends. No corpus name's `capitalized()` moved (the design-docs review compared every one across the two trees).
A GAP THE RULE DOES NOT CLOSE, found by the design-docs review and present on 16ee2851 too: under `Policy(lenient_comma_suffixes=False)` the strict comma-suffix test asks a one-letter numeral's shape as written, not its tag, so `john smith, v` reads suffix 'v' where `JOHN SMITH, V` reads given 'V', and `smith, john, i` reports nothing where `SMITH, JOHN, I` reports `comma-structure`. The committed grid runs the default policy only. Left for its own change.
Open: #727 — whether the strict comma test should read the tag.
A GAP THE RULE DOES NOT CLOSE, found by the design-docs review and present on 16ee2851 too: under `Policy(lenient_comma_suffixes=False)` the strict comma-suffix test asks a one-letter numeral's shape as written, not its tag, so `john smith, v` reads suffix 'v' where `JOHN SMITH, V` reads given 'V', and `smith, john, i` reports nothing where `SMITH, JOHN, I` reports `comma-structure`. The committed grid runs the default policy only. Left for its own change, and taken by the #727 entry below (was Open: #727).
The connective letters are left where P3 put them. Among the name's own words, P3's fork answers by vocabulary before this rule is asked — the marked `e` and `i` read as initials and report, the unmarked `y` as a connective — and does so in both spellings, so the invariant already held for them; a connective letter outside the own words was always read by the bare-capital shape in its capital spelling, and its lower-case spelling now follows.
- 2026-10-18, #723's code review — A LETTER OUTSIDE THE OWN WORDS ASKS THE WHOLE NAME'S CASE. The first version gated the new tag on the one-case fact classify records, which is asked of the name's own words only (P3), so a lower-case letter in a clause of a name whose own words are capitals passed it: `MARIA GARCIA NEE LOPEZ y FERNANDEZ` and `MARIA GARCIA (LOPEZ y FERNANDEZ)` tagged the `y` an initial (case repair wrote `Lopez Y Fernandez`), and `DR. NÉE JUAN y LOPEZ` moved from family 'JUAN y LOPEZ' to middle 'JUAN y' — mixed-case writing, which this rule reads as no initial, and moves the twin-only grids could not see, since every pair in them is wholly one case. The fix asks a word outside the own words about the whole name instead; an own word keeps the own-words fact, so a clause beside the name still changes nothing about the name's own letters (P3). The defect predated #723 for the numeral letters, which #721 gated the same way; they are read by this rule now and move with it, and the move is not only a tag (found by the design-docs review). After a marker among capitals, a lower-case numeral no longer holds the clause up as an initial would: `MARY SMITH NEE v JONES` reads middle 'SMITH NEE v', family 'JONES' (on 16ee2851, maiden 'v JONES'), `MARY SMITH NEE i JONES` family 'NEE i JONES', and `JANE DOE NEE SMITH v` drops `suffix-or-name`. Each is the reading of its mixed-case spelling (`Mary Smith nee v Jones`) and of 2.4.2 (and 1.4.0 for the first), so only the unreleased #721 had moved them and no release bullet is owed; the capital twin `MARY SMITH NEE V JONES` keeps maiden 'V JONES'. Pinned by the row `a_lowercase_numeral_after_a_marker_among_capitals_is_no_initial`. Pinned by `test_a_lowercase_clause_letter_among_capitals_is_no_initial` and the case row `a_lowercase_letter_past_a_declined_marker_among_capitals_joins`. The first entry's figures were measured on the first version, and the twin grids are unchanged by the fix (both spellings of every pair in them are wholly one case).
- 2026-10-18, #727 — THE STRICT SUFFIX TEST VETOES A ONE-CASE LETTER AS IT VETOES ITS CAPITAL. The gap the first entry recorded: `is_suffix_strict` vetoes an initial by the word's text (`is_initial`), not by classify's tag, so a one-case lowercase `v` or `i` — an initial under this rule — passed where its capital was vetoed. Two callers reached it. Under `Policy(lenient_comma_suffixes=False)`, C1's comma tests (`is_wholly_suffix` and the name-word count): `john smith, v` read suffix 'v' where `JOHN SMITH, V` reads given 'V', and `smith, john, i` reported nothing where its capitals report `comma-structure`. And under the default policy, W2's scan-back (`_script_segment._is_post_nominal`): `田中さん v` peeled さん and read suffix 'さん v' where `田中さん V` reads given '田中さん', family 'V' — a CJK name with a Latin letter beside it is one case under the own-words test, the letter being the only cased word in it, so nothing in the writing contrasts it. The strict test now takes the one-case fact as an argument and vetoes a bare lowercase letter whose capital is A to Z where it is true; segment, `_comma` and script_segment pass the fact they already hold, and segment asks for it only under the strict knob. A separate keyword from `is_wholly_suffix`'s `one_case`, which is the credential LEAN and is None by design at callers that want no lean; they still want the letters read by this rule. The lenient default takes a listed suffix word whatever its shape and never reads the argument.
Measured: the committed invariant's grid gains the three comma shapes and `田中さん {n}`, and runs every comma-holding shape again under the strict knob (the knob reaches nothing else); 2444f847 disagreed on 72 of its 4,554 pairs (54 strict comma pairs, 18 honorific), this tree on none, and the third recorded control is that 72. `田中さん v` now reads as 1.4.0 read it; 2.1 through 2.4.2 peeled it (suffix 'さん, v' in 2.1 and 2.2, 'さん v' from 2.3). No corpus name moved.
Open: none from this entry; #727 closes the gap the first entry left open.
6 changes: 4 additions & 2 deletions docs/design/rules/C1-suffix-comma-decision.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,8 @@ C1. Rationale: a credential run after the comma means the name is in
parse could not recognize, not a fork it called.
By default a recognized suffix word counts
even written like an initial ("V."), while strict mode vetoes
initial-shaped words. In the listing form the part after the
a word read as an initial — in a name written in one case a
bare lower-case letter as well, as its capital is (T4). In the listing form the part after the
comma is still read for what it is: a part that is nothing but
suffix words is the credential run and reads as suffixes, whole
— the slot after a family comma is postnominal position, so the
Expand Down Expand Up @@ -195,6 +196,7 @@ C1. Rationale: a credential run after the comma means the name is in
"John Smith, PhD" → suffix="PhD"
"John Smith, V." → suffix="V."
"John Smith, V." strict-comma-suffixes → family="John Smith"
"john smith, v" strict-comma-suffixes → family="john smith"
"Smith, PhD" → family="Smith" · boundary
"Smith, PhD" → suffix="PhD"
"Smith, Jr." → suffix="Jr."
Expand Down Expand Up @@ -348,5 +350,5 @@ C1. Rationale: a credential run after the comma means the name is in
V` reads the suffix and `Smith, John PhD I.` continues the run,
while adding a suffix comma after either turns that same letter
into the middle initial.
history: decisions/C1 · interacts: H1, H2, H6, P1, P2, P3, P5, P6, W3, S3, M2, S5, S6, S7, S8 · implemented: nameparser/_pipeline/_comma.py, nameparser/_pipeline/_assign.py, nameparser/_pipeline/_group.py
history: decisions/C1 · interacts: H1, H2, H6, P1, P2, P3, P5, P6, W3, S3, M2, S5, S6, S7, S8, T4 · implemented: nameparser/_pipeline/_comma.py, nameparser/_pipeline/_assign.py, nameparser/_pipeline/_group.py

8 changes: 5 additions & 3 deletions docs/design/rules/T4-bare-letter-as-an-initial.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ T4. Rationale: one letter standing for a name word is how an
an initial differently from a name word reads the lower-case
spelling as it reads the capitals: M4 leaves an initial its
given-name reading, O5 reports no convention for one, P3's
count passes over one, and S10 reads a numeral written as one
as the capital numeral it is. A letter whose capital is not one
count passes over one, S10 reads a numeral written as one
as the capital numeral it is, and a suffix test that vetoes an
initial vetoes it — C1's strict mode, and the scan-back of W2's
peel. A letter whose capital is not one
of those — é, ж — is no initial in either spelling, and in a
name of mixed case a bare lower-case letter is no initial. As
P3 asks it, the case question is asked of the name's own words,
Expand All @@ -42,4 +44,4 @@ T4. Rationale: one letter standing for a name word is how an
"DR. NÉE JUAN y LOPEZ" → family="JUAN y LOPEZ" · boundary
"jose e lopez" → ambiguities=("conjunction-or-initial",)
"jose y lopez" → middle="y"
history: decisions/T4 · interacts: M4, O5, P2, P3, P7, S10, R3, R4 · implemented: nameparser/_pipeline/_classify.py
history: decisions/T4 · interacts: M4, O5, P2, P3, P7, S10, R3, R4, C1, W2 · implemented: nameparser/_pipeline/_classify.py, nameparser/_pipeline/_vocab.py
2 changes: 1 addition & 1 deletion docs/design/rules/W2-glued-honorific-peel.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,5 @@ W2. Rationale: some East Asian honorifics glue directly onto the end
more right (#707).
"田中 副社長" → given="副"
"岸田文雄衆議院議員" → family="岸田文雄衆議院"
history: decisions/W2 · interacts: W3 · implemented: nameparser/_pipeline/_script_segment.py
history: decisions/W2 · interacts: W3, T4 · implemented: nameparser/_pipeline/_script_segment.py

2 changes: 1 addition & 1 deletion docs/release_log.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Release Log
- **Fix case repair capitalizing the Dutch particle 't.** ``jacobus henricus van 't hoff`` repairs to ``Jacobus Henricus van 't Hoff``, where 1.4.0 through 2.4.2 gave ``van 'T Hoff``. :issue:`700`
- **Add Gov as a title.** ``Gov Smith`` gives title ``Gov``, last ``Smith``, where 1.4.0 through 2.4.2 gave first ``Gov``, and ``Lt. Gov. Smith, John`` gives title ``Lt. Gov.`` with the change above. :issue:`701`
- **Fix a capital V, I or X after a lone first name being read as a generational suffix, leaving no last name.** ``John V`` gives first ``John``, last ``V``, and ``Malcolm X`` last ``X``, where 2.0.0 through 2.4.2 gave suffix ``V`` and ``X`` with no last name, restoring 1.4.0's reading. A title is no word to spare, so ``Dr. Smith V`` gives first ``Smith``, last ``V`` (1.4.0's reading too) where 2.0.0 through 2.4.2 gave last ``Smith``, suffix ``V``; a name with a word to spare keeps the suffix, and ``John Smith V`` still gives suffix ``V``. A name written all in lower case reads the same way (``john v`` gives last ``v``). :issue:`718`, :issue:`721`
- **Fix a single letter in a name written all in lower case reading differently from the same name in capitals.** ``jose j y lopez`` gives first ``jose``, middle ``j y``, last ``lopez``, as ``JOSE J Y LOPEZ`` does, where 1.4.0 through 2.4.2 gave last ``j y lopez``. A lone letter before a maiden marker stays the first name (``j nee smith`` as ``J NEE SMITH``). :issue:`723`
- **Fix a single letter in a name written all in lower case reading differently from the same name in capitals.** ``jose j y lopez`` gives first ``jose``, middle ``j y``, last ``lopez``, as ``JOSE J Y LOPEZ`` does, where 1.4.0 through 2.4.2 gave last ``j y lopez``. A lone letter before a maiden marker stays the first name (``j nee smith`` as ``J NEE SMITH``), ``田中さん v`` reads as ``田中さん V`` does, and ``Policy(lenient_comma_suffixes=False)`` reads ``john smith, v`` as it reads ``JOHN SMITH, V``. :issue:`723`, :issue:`727`

* 2.4.2 - October 13, 2026

Expand Down
13 changes: 7 additions & 6 deletions nameparser/_pipeline/_classify.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,12 +171,13 @@ def _tags_for(token: WorkToken, n: str, state: ParseState,
# nothing about any letter in it, a bare letter is an initial
# wherever its capital would be" -- so every reader of the tag
# after classify reads 'j' as it reads 'J' (#723; the roman
# numerals first, #721) -- not the honorific peel upstream,
# which asks the shape itself ('田中さん v' still reads
# otherwise than '田中さん V'). `one_case` is the fact about
# the name's own words, applied to a clause's words too, as
# #516's caps branch applies it, since a capital 'J' in a
# clause is an initial as well. The capital is asked of the
# numerals first, #721). Readers that run before the tag
# exists, or ask the word's text rather than its tag -- the
# strict suffix test, which the honorific peel and
# `lenient_comma_suffixes=False` ask -- take the same fact as
# an argument (#727). `letter_one_case` is the own-words fact
# for an own word and the whole name's for a clause's
# (docstring). The capital is asked of the
# bare pattern at C level, no frame per one-letter word, and
# needs no script test: no capital `_INITIAL` matches is in a
# script without initials. 'é' and 'ж' stay as they were,
Expand Down
9 changes: 6 additions & 3 deletions nameparser/_pipeline/_comma.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,8 @@ def decide(state: ParseState) -> ParseState:
licensed: set[int] = set()
run = False
if maybe:
names = name_word_count([tokens[i].text for i in seg0], lex, pol)
names = name_word_count([tokens[i].text for i in seg0], lex, pol,
letters_one_case=state.one_case)
if names >= 2:
# rules/C1: "An unlisted all-caps word joins the class in such
# a part only where the name carries the contrast"
Expand Down Expand Up @@ -283,7 +284,8 @@ def decide(state: ParseState) -> ParseState:
for i in p:
others.append(tokens[i].text)
run = (bool(licensed)
and (not others or is_wholly_suffix(others, lex, pol))
and (not others or is_wholly_suffix(
others, lex, pol, letters_one_case=state.one_case))
and not any(len(p) == 1
and is_single_letter_numeral(tokens[p[0]].text)
for p in p1))
Expand Down Expand Up @@ -336,7 +338,8 @@ def decide(state: ParseState) -> ParseState:
# rules/C1: "By default a recognized suffix word counts even
# written like an initial" -- behind a whole name ('John Smith, V.')
if reading is None and whole and is_wholly_suffix(
[tokens[i].text for i in part], lex, pol, one_case=None):
[tokens[i].text for i in part], lex, pol, one_case=None,
letters_one_case=state.one_case):
reading = (True,) * len(p1)
if reading is None:
return state
Expand Down
8 changes: 4 additions & 4 deletions nameparser/_pipeline/_pieces.py
Original file line number Diff line number Diff line change
Expand Up @@ -800,10 +800,10 @@ def segment_suffix_reading(pieces: Sequence[Sequence[int]],
title to read by).

`lenient` is Policy.lenient_comma_suffixes, and only the numeral
continuation consults it. C1: "by default a recognized suffix word
counts even written like an initial, while strict mode vetoes
initial-shaped words" -- so under strict the veto stands and the
run ends where it always did. Reading no policy here silently
continuation consults it. C1: "while strict mode vetoes a word read
as an initial" -- so under strict the veto stands and the run ends
where it always did; the walk reads the token's `initial` tag, so a
one-case lowercase letter is vetoed as its capital is (rules/T4). Reading no policy here silently
overrode the one knob a caller sets to prevent exactly this.

The FAMILY_COMMA rule that segment 0 is the family name rests
Expand Down
Loading
Loading