Skip to content

Commit be1c32b

Browse files
authored
Merge pull request #517 from derek73/claude/471-order-declarations
Order declarations are authoritative: #471 declined, #384 on option 1 with the rotations gated
2 parents 8e6c483 + 0b6a792 commit be1c32b

9 files changed

Lines changed: 286 additions & 39 deletions

File tree

docs/design/decisions.md

Lines changed: 17 additions & 8 deletions
Large diffs are not rendered by default.

docs/design/rules.md

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -134,20 +134,23 @@ P1. Rationale: a particle OPENING a name has the whole rest of the
134134
How MANY name words it attaches to depends on the order. Under a
135135
family-first order it takes exactly ONE — declaring that order
136136
asserts that what follows the family is not more surname. Under
137-
the default order it takes the rest of the name: nothing there
138-
marks where the surname ends. Whether the default order should
139-
narrow the same way is #471 — today `de Mesnil Jean` reports the
140-
whole string as the family, and that reach is 1.4.0's. One name
141-
word means one UNIT — a particle chain (P2), a conjunction join
142-
(P3) or a bound given-name pair (P5) is taken whole or not at
143-
all. A title does not move the opening position (P4), but a
144-
family comma does end the question: the comma has already fixed
145-
the surname, so there is no positional read left for an order to
146-
narrow, and a particle opening the part AFTER it takes the rest
147-
of that part whatever order is declared. What is left over is not read by O4's rule for a whole name,
148-
which would make the first leftover a second family name; it is
149-
laid out as the positions AFTER the family in the declared order,
150-
the family slot being already filled.
137+
the default order it takes the rest of the name, and that is a
138+
reading rather than a gap in one: a never-given particle marks
139+
where a surname BEGINS, surnames are routinely several words,
140+
and no declaration has said this one ends before the string
141+
does. So `de Mesnil Jean` reports the whole string as the
142+
family, which is 1.4.0's reach as well (#471, declined by
143+
design). One name word means one UNIT — a particle chain (P2),
144+
a conjunction join (P3) or a bound given-name pair (P5) is
145+
taken whole or not at all. A title does not move the opening
146+
position (P4), but a family comma does end the question: the
147+
comma has already fixed the surname, so there is no positional
148+
read left for an order to narrow, and a particle opening the
149+
part AFTER it takes the rest of that part whatever order is
150+
declared. What is left over is not read by O4's rule for a
151+
whole name, which would make the first leftover a second family
152+
name; it is laid out as the positions AFTER the family in the
153+
declared order, the family slot being already filled.
151154
"de la Vega" → family="de la Vega"
152155
"Sir de Mesnil" → family="de Mesnil"
153156
"Mesnil de" family-first → given="de"
@@ -161,6 +164,18 @@ P1. Rationale: a particle OPENING a name has the whole rest of the
161164
"Mc Donald" → family="Mc Donald"
162165
"de los Santos" → family="de los Santos"
163166
"van Gogh" → given="van" · boundary
167+
Accepted: the default-order reach has two faces, and the second
168+
is a real cost. A surname-only string is read right, which is
169+
what the reach is for; a family-first listing read under the
170+
default order is read wrong, all of it landing in the family.
171+
The remedy is the declaration or the comma — declare a
172+
family-first order, which gives family `de la Cruz`, given
173+
`Juan`, middle `Carlos`, or write the family comma. What
174+
NEITHER order settles is the comma's alone: `de la Family
175+
Family Given` and `de la Family Given Given` are the same
176+
string shape, and no reading of the words tells them apart.
177+
"de la Torre Vega" → family="de la Torre Vega"
178+
"de la Cruz Juan Carlos" → family="de la Cruz Juan Carlos"
164179
Accepted: a bare "de" stays the given name — there is nothing to
165180
fold into, and inventing a surname would be worse.
166181
"de" → given="de"
@@ -951,6 +966,7 @@ C2. Rationale: text beyond the recognized comma parts should be
951966
## Name order (O)
952967

953968
Background: written name order varies by convention: given-first (the library's default reading), family-first, and family-first with the given name last (Vietnamese, where the person is called by the last element, given names are frequently two syllables — the given_names view stays correct wherever the internal boundary falls — and quốc ngữ is Latin script, so no native-script signal exists at all). The order is declared by the caller or a locale pack, never detected — but a few conventions leave a recognizable trace in the name itself. Patronymics are one: East Slavic names carry a father's-name derivative with distinctive endings between given and family, and Turkic names use a standalone marker word ("oglu" son-of, "qizi" daughter-of) after the father's name. Where such a trace is present and unambiguous, an opted-in parser can restore the intended reading from a family-first listing.
969+
A declared order is a property of the DATA SOURCE rather than of any one string: the caller sets it to match how their records are written, and it governs what no vocabulary and no script license has already claimed (O4). A declared FAMILY-FIRST order outranks what the parser could infer from the shape of a particular name; the given-first reading is the parser's DEFAULT rather than a declaration it can tell apart from one, and the traces above are what refine it (O1, O2). And the declaration yields where a name's own script settles the order instead (W4, where a name written wholly in an East Asian script reads family-first whatever order the caller declared). Two consequences run through this document. Under the default given-first order a string opening with a never-given particle is a surname whose given name is simply absent, so the fold takes the rest of it (P1). Under a declared family-first order the caller has already said that what follows the family is not more surname — so the fold stops there, and the rotations below, whose whole job is to RESTORE the default reading from a family-first listing, have nothing left to restore. A shape neither order settles is the family comma's job, and the parser does not guess at it.
954970

955971
O1. Rationale: an East Slavic name written family-first still shows
956972
its patronymic — the distinctive ending identifies which word is
@@ -962,9 +978,26 @@ O1. Rationale: an East Slavic name written family-first still shows
962978
the words are restored to given, patronymic, family. A middle
963979
word that also carries the ending blocks the reading, because
964980
the surname itself may be patronymic-derived.
981+
The restoration is the default order's work: it recovers the
982+
given-first reading a family-first listing hides, so where the
983+
caller has DECLARED a family-first order there is nothing left
984+
to restore and position decides.
965985
"Сидоров Иван Петрович" [ru] → family="Сидоров"
966986
"Sidorov Ivan Petrovich Jr." [ru] → family="Sidorov"
967987
"Иван Петрович Абрамович" [ru] → family="Абрамович" · boundary
988+
Accepted: under a declared family-first order the readings part
989+
on natural-order input, and the declaration wins. A family-first
990+
listing reads the same either way under FAMILY_FIRST — the first
991+
example above is that same parse with FAMILY_FIRST declared,
992+
and under FAMILY_FIRST_GIVEN_LAST the listing's given and middle
993+
swap, so only the family is invariant — while with East Slavic
994+
handling active and FAMILY_FIRST declared the natural-order
995+
Иван Петрович Сидоров reads family Иван, given Петрович, middle
996+
Сидоров. That is the caller's declaration being honored on input
997+
they said was written family-first, not a defect (#384). No
998+
registered example annotation combines a pack with an order, so
999+
the parse is pinned in tests/v2/test_locales.py instead, and
1000+
decisions.md#O1 records why options 2 and 3 were declined.
9681001
history: decisions.md#O1 · implemented: nameparser/_pipeline/_post_rules.py
9691002

9701003
O2. Rationale: a Turkic patronymic marker is a separate word that
@@ -975,12 +1008,19 @@ O2. Rationale: a Turkic patronymic marker is a separate word that
9751008
nicknames aside — ending in a standalone patronymic marker reads
9761009
family-first: the first name word is the family name, and the
9771010
marker stays beside the father's name in the middle.
1011+
The scope is O1's: the restoration recovers the default reading,
1012+
so a declared family-first order stands in its place.
9781013
"Ali Ahmad Vali oglu" [tr_az] → family="Ali"
9791014
"Ali Ahmad Vali oglu Jr." [tr_az] → family="Ali"
9801015
Accepted: any other count of name words keeps its positional
9811016
reading, even when that leaves the marker itself in a name
9821017
field.
9831018
"Ali Ahmad oglu" [tr_az] → family="oglu" · boundary
1019+
Accepted: the order scope above has this rule's own consequence
1020+
reached by a second route — with Turkic handling active and
1021+
FAMILY_FIRST_GIVEN_LAST declared, Ali Ahmad Vali oglu reads
1022+
given oglu, the marker standing in a name field because the
1023+
declaration put the name's last word there (#384).
9841024
history: decisions.md#O2 · implemented: nameparser/_pipeline/_post_rules.py
9851025

9861026
O3. Rationale: several traditions write compound family names

docs/release_log.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ Release Log
1818

1919
- **Mark ba as an acronym that is also an ordinary name, so a bare trailing Ba keeps the family name.** ``HumanName("Anna Ba")`` gives last ``Ba`` and reports a suffix-or-name ambiguity, where 2.0.0 through 2.2.0 gave suffix ``Ba`` and no last name. The SPACED full-name form keeps the credential reading: ``John Smith BA`` still gives suffix ``BA``, now flagged, and the dotted ``John Smith B.A.`` is an unflagged suffix, the periods settling it. The COMMA forms move, and this is the marking's real cost: ``Smith, BA`` gives first ``BA``, and ``John Smith, BA`` gives first ``BA``, last ``John Smith``, where 2.0.0 through 2.2.0 gave suffix ``BA`` for both -- what ``Smith, Ed`` costs, which S2 already accepted for the other ambiguous acronyms. A bracketed or quoted ``John Smith (BA)`` falls through to nickname parsing, as the 2.0 note for ``ma``/``do`` below recorded for that pair. Write ``B.A.`` to keep the credential reading. BA is a common credential and Ba a real surname in Vietnamese and Senegalese Fula, which is the ``ma``/``Ma`` shape exactly. No corpus name moves (#342)
2020

21+
- **Fix the East Slavic and Turkic patronymic rotations overriding a declared family-first name order.** With ``patronymic_rules`` opted in and ``Policy(name_order=FAMILY_FIRST)``, ``Мицкевич Адам Юзеф`` gave last ``Адам`` through 2.2.0 and now gives last ``Мицкевич`` -- the reading the declaration asks for -- and ``oglu Ahmad Vali Ali`` with Turkic handling gave last ``Ahmad`` and now ``oglu``. The rotations exist to restore the given-first reading a family-first listing hides, so under a declared family-first order the declaration decides. No corpus name moves. See the ``O1`` entry of ``docs/design/decisions.md`` (closes #384)
22+
2123
**Additions**
2224

2325
- **Add the renunciate titles to the given-name title list, so a renunciate's one name is a given name.** ``HumanName("Swami Vivekananda")`` gives first ``Vivekananda`` with an empty last name, where every release since 1.4.0 gave last ``Vivekananda``; ``Guru Nanak``, ``Baba Ramdev`` and ``Lama Zopa`` move the same way, and so do the Devanagari and Bengali spellings added below. Two name words behind the title are unchanged -- ``Swami Vivekananda Saraswati`` keeps last ``Saraswati`` -- and a surname-retaining title is untouched: ``Rabbi Cohen`` still gives last ``Cohen``. ``venerable`` is deliberately not in the list, the traditions using it splitting on whether the family name survives. See the ``indic-honorifics`` entry of ``docs/design/decisions.md`` (closes #346)

nameparser/_pipeline/_post_rules.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,23 @@
6565
_RENDERS_ELSEWHERE = frozenset({Role.TITLE, Role.NICKNAME, Role.MAIDEN})
6666

6767

68+
def _rotations_apply(state: ParseState) -> bool:
69+
# Both patronymic rotations RESTORE the given-first reading a
70+
# family-first listing hides, so rules.md#O1's scope clause holds
71+
# them to the default order: a caller who declared family-first has
72+
# already said what the rotation would infer, and position decides
73+
# (decisions.md#O1, the 2026-09-07 entry on #384). `state.order`,
74+
# not policy.name_order, for the reason the P1 fold gives -- a
75+
# script_orders entry can override the policy, and the roles the
76+
# rotations read are the ones assign actually made. None means
77+
# assign positioned nothing: a family comma (which the NO_COMMA
78+
# test already excludes) or an early return with no name piece to
79+
# position, so there is no declaration to defer to and the
80+
# rotation's own shape test decides.
81+
return state.structure is Structure.NO_COMMA and (
82+
state.order is None or state.order[0] is Role.GIVEN)
83+
84+
6885
def _mark_suffix_entries(tokens: list[WorkToken], state: ParseState) -> None:
6986
# In place over the caller's token list, the way every other rule
7087
# in post_rules writes: a state-in/state-out spelling here cost
@@ -462,7 +479,7 @@ def post_rules(state: ParseState) -> ParseState:
462479
# middle_as_family fold below runs comma or not (v1 order:
463480
# patronymics first, then handle_middle_name_as_last)
464481
rules = state.policy.patronymic_rules
465-
rotations_apply = state.structure is Structure.NO_COMMA
482+
rotations_apply = _rotations_apply(state)
466483
# rules.md#O1: "a name of exactly three name words — titles,
467484
# suffixes and nicknames aside — whose last name word carries a
468485
# patronymic ending and whose middle name word does not reads as

tests/v2/cases.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -691,7 +691,11 @@ def _check_cjk_shape_purity(self) -> None:
691691
"ambiguous, so this fold cannot fire on it. See "
692692
"decisions.md#P1, 2026-08-17.) 1.4.0 gives last 'de la "
693693
"Cruz Juan Carlos' too, so this row must not move when "
694-
"#395 lands"),
694+
"#395 lands. #471 DECLINED 2026-09-07: the reach is "
695+
"the reading and not a gap in one -- a declared order "
696+
"is a property of the data source, so under the "
697+
"default order nothing has said this surname ends "
698+
"before the string does. See decisions.md#P1"),
695699
Case("leading_never_given_particle_two_leftovers_family_first",
696700
"de la Cruz Juan Carlos",
697701
{"family": "de la Cruz", "given": "Juan", "middle": "Carlos"},

0 commit comments

Comments
 (0)