1from nameparser.config._invariants import assert_normalized
2from nameparser.config.bound_given_names import BOUND_GIVEN_NAMES
3
4#: The sub-set of :py:data:`PARTICLES` that no culture uses as a standalone
5#: given name. Where one of these stands ALONE as the piece opening a name, that
6#: name has no given name -- the whole thing is a surname (e.g. "de Mesnil"
7#: -> family name "de Mesnil") -- and that reading holds under EVERY
8#: ``name_order`` (#359). It is not scoped to the default order the way the
9#: rest of the positional read is: what settles it is that a particle
10#: opening the name has the rest of the name to JOIN FORWARD TO, which
11#: is evidence about the writing rather than about the word, so
12#: ``Policy(name_order=FAMILY_FIRST)`` reads "de Mesnil" as the family
13#: name too. Membership here does NOT mean the word can never be a
14#: given name -- where position forces the reading it is one, and every
15#: member of this set reports given "de"-style for a bare parse. What is asked about is the
16#: opening *piece*, not the first word of the string: a particle that has
17#: already chained onto the word behind it is part of that piece rather
18#: than standing alone.
19#: OPENING the name is the whole of that rule's subject (rules.md#P1;
20#: the pre-2.2 docstrings called it rule 1b). A member standing alone
21#: where a declared order puts the GIVEN name was a second site until
22#: #467, and is not one now: that slot holds what the caller declared,
23#: so ``Policy(name_order=FAMILY_FIRST)`` reads "Mesnil de" as family
24#: "Mesnil", given "de". A member ENDING the name under a family-first
25#: order is rules.md#P6's, which joins it to the family. A bare "de" stays as it is. Where a chain is
26#: reported as the given name anyway it is because the member is no
27#: longer standing alone: under ``Policy(name_order=FAMILY_FIRST)`` the
28#: given position of "Juan de la Vega" holds the whole three-token
29#: chain, so the rule declines and given "de la Vega" stands. A title
30#: in front is NOT such a case -- since #367 a title is transparent to
31#: the leading-particle exception, so "Sir de Mesnil" leaves "de" a lone
32#: piece and reads family "de Mesnil", exactly as the untitled form does.
33#: Membership also decides the ambiguity report -- see
34#: :py:data:`PARTICLES` below.
35#: Curated to exclude anything that can be a given name in some culture
36#: (`al`, `van`, `von`, `della`, `di`, `del`, `da`, `vander`, ...) and
37#: anything that is also a bound given-name particle (`abu`). When unsure,
38#: leave a word out: a missing member just means that name is not
39#: auto-fixed, whereas a wrong member misparses a real person. Must stay a
40#: subset of :py:data:`PARTICLES` and disjoint from
41#: :py:data:`~nameparser.config.bound_given_names.BOUND_GIVEN_NAMES`.
42NON_GIVEN_NAME_PARTICLES = frozenset({
43 # Latin-script members. Every entry here is a grammatical particle --
44 # an article, a preposition, or a patronymic marker -- and that is the
45 # form a never-given record has to take. One attested bearer settles
46 # the AMBIGUOUS side; no amount of searching settles absence, so what
47 # is recorded is what the word IS rather than a failed search.
48 #
49 # The test is POSITIONAL (decisions.md#vocabulary-collisions C-i,
50 # corrected 2026-08-17): membership asks whether the word is borne as
51 # a name in the position this rule ACTS on -- leading, or alone in the
52 # given role -- not whether a bearer exists anywhere. 'de' is why the
53 # qualifier is needed: "De" is a borne Bengali/Odia surname, but a
54 # TRAILING one, so it never meets the rule.
55 #
56 # A BARE entry, here or in PARTICLES below, means nobody has examined
57 # it. Absence of a comment is the audit record.
58 "'t", # Dutch contraction of 'het' ("'t Hooft"): the article itself
59 'af', # Danish/Norwegian nobiliary "of"
60 'auf', # German "upon" ("auf der Heide")
61 'av', # Swedish/Norwegian "of"
62 'bint', # Arabic "daughter of"; native-script بنت below
63 'das', # Portuguese/German "of the", fem. pl. Added #360: its
64 # own plural partner 'dos' was already here. "Das" IS a
65 # common Bengali surname, but a TRAILING one -- measured,
66 # "Anjali Das" and "Bimal Das" are untouched, while
67 # "Maria das Neves" gains the particle it loses today.
68 'de', # French/Iberian/Italian "of". "De" IS a borne Bengali and
69 # Odia surname -- trailing, so it never reaches this rule.
70 # The case that forced C-i's positional qualifier.
71 "de'", # Italian elided "dei" ("de' Medici")
72 'degli', # Italian "of the", masc. pl.
73 'dei', # Italian "of the", masc. pl.
74 'delle', # Italian "of the", fem. pl.
75 'delli', # Italian "of the", regional variant of 'dei'
76 'dello', # Italian "of the", masc. sg.
77 'dem', # German dative article
78 'der', # German article ("von der Leyen")
79 'dos', # Portuguese "of the", masc. pl.
80 'het', # Dutch definite article
81 'ibn', # Arabic "son of"; native-script ابن below
82 'las', # Spanish "the", fem. pl. Added #360: 'la' was here as
83 # AMBIGUOUS (La Shawn, La Toya) but the plurals are not
84 # name elements -- no bearer in any position.
85 'los', # Spanish "the", masc. pl. Same as 'las' -- and the pair
86 # is why the #390 fold narrowing regressed
87 # "de los Santos" to given='Santos', family='de los'.
88 'mc', # Contraction of 'Mac'; unlike 'mac' (Mac Miller) it is
89 # not a standalone given name. Measured misparse: "Mc
90 # Donald" gave given='Mc'. Also in SUFFIX_ACRONYMS as
91 # Master of Ceremonies, but that is trailing position and
92 # unaffected -- "John Smith MC" still reads suffix (#360).
93 'op', # Dutch "at/on" ("op den Berg")
94 'ste', # Contraction of 'Sainte'; has a vowel, which is why the
95 # criterion is "abbreviation of a word that is never
96 # itself a name" rather than the vowel shape #360 first
97 # proposed. Measured misparse: "Ste Marie" gave
98 # given='Ste'.
99 'ter', # Dutch "at the" ("ter Horst")
100 'vd', # Dutch abbreviation of "van der". Also the British
101 # Volunteer Decoration, a suffix acronym: two non-name
102 # readings, so C-ii decides it on frequency and the Dutch
103 # one wins (decisions.md#vocabulary-collisions).
104 'vom', # German "from the"
105 'zu', # German "at/to", nobiliary ("zu Guttenberg")
106
107 # #269: Arabic native-script patronymic/clan particles. Unlike their
108 # Latin transliterations, these live in a script namespace with no
109 # collision against an unrelated Latin given name, so each is judged
110 # on its own semantics rather than mirrored blindly:
111 'بن', # "bin"/"ibn" (son of) -- never a bare given name. Latin
112 # 'bin' is in PARTICLES but not in this set; that judgment
113 # is unchanged by adding the Arabic-script form.
114 'بنت', # "bint" (daughter of) -- mirrors Latin 'bint' above.
115 'ابن', # "ibn" (son of, alternate spelling) -- mirrors Latin
116 # 'ibn' above.
117 'آل', # "aal" (family/clan of, e.g. "Al Saud") -- distinct from
118 # the excluded definite article "ال" (#269 explicitly
119 # excludes standalone "ال"); a clan prefix, never a bare
120 # given name.
121
122 # #269: Hebrew native-script patronymic particles -- same
123 # reasoning as the Arabic ones above: no Latin-script collision,
124 # and neither functions as a standalone given name in Hebrew usage.
125 # Deferred under the collision rule: 'בר' (Aramaic son-of, as in
126 # Bar-Lev) -- Bar is a common modern Israeli given name, and the
127 # surname spelling is hyphenated anyway.
128 'בן', # "ben" (son of)
129 'בת', # "bat" (daughter of)
130})
131
132# Maintainer note, deliberately a plain comment ABOVE the `#:` run
133# rather than inside it: `#:` would publish it into the API reference,
134# where it is advice to nobody, and a plain comment placed *within* the
135# run splits it -- autodoc then renders only the fragment below the
136# split and silently drops everything above it. Frozen by construction
137# (#293) -- `frozenset | set` returns a frozenset, the LEFT operand's
138# type wins, so keep the frozenset first. Flipped, this silently yields
139# a plain set again and unfreezes the constant.
140#: Family-name particles: a particle joins to the piece that follows it
141#: to make one new piece, and particles chain, e.g. "von der" and
142#: "de la". A particle in a non-leading position also pulls the pieces
143#: after it into the same one, up to the next particle run or suffix,
144#: which is how a multi-word name piece is recognized. Where that piece
145#: lands is a later question: in "pennie von bergen wessels MD", "von"
146#: joins each following piece until the suffix "MD", giving the family
147#: name "von bergen wessels", while the same chaining in "Smith, Juan
148#: de la Cruz" gives the middle name "de la Cruz". A leading
149#: particle is the exception and chains nothing: the chain skips the
150#: first piece of the NAME, its membership in this set or in
151#: :py:data:`NON_GIVEN_NAME_PARTICLES` never entering into it. Leading
152#: is read off the name rather than off the input (#367): a title is
153#: not part of the name, so it is stepped over and "Dr. Van Johnson"
154#: reads as the untitled "Van Johnson" does. One kind of word is not
155#: stepped over, and this is the one place a title's vocabulary and
156#: this one interact -- ``st``, ``do`` and ``freiherr`` are each BOTH
157#: a title and a particle, so any of them could be the name's own
158#: first piece and stops the scan, leaving a particle behind it
159#: non-leading and free to chain ("Freiherr von Richthofen").
160#: Where the pieces then land is again a later
161#: question, and this is where membership decides something: a leading
162#: :py:data:`NON_GIVEN_NAME_PARTICLES` member makes the whole name a
163#: family name ("de la Vega") under every ``name_order``, because a word
164#: that is never a given name leaves the order nothing to place. A
165#: leading particle OUTSIDE that set could be either, so there
166#: ``name_order`` decides after all: the default given-first order reads
167#: it as the given name ("Van Johnson"), while either family-first order
168#: splits the same chains-nothing grouping the other way round ("Van
169#: Johnson" -> family "Van", given "Johnson").
170#: What membership decides under ANY of the three orders is also the
171#: report: a leading particle outside
172#: :py:data:`NON_GIVEN_NAME_PARTICLES` records a particle-or-given
173#: ambiguity for the reading not taken, and one inside it records none.
174#:
175#: Defined as a static union so every :py:data:`NON_GIVEN_NAME_PARTICLES`
176#: member is guaranteed to also be a particle (and still join forward),
177#: with no drift -- mirroring ``TITLES = GIVEN_NAME_TITLES | {...}`` in
178#: :py:mod:`nameparser.config.titles`.
179# The AMBIGUOUS half: words that are also borne as names in the position
180# the leading-particle rule acts on (decisions.md#vocabulary-collisions
181# C-i). A commented entry has been examined; a BARE one is sitting on the
182# conservative default and nobody has looked at it. #360 examined the 12
183# that appear leading in tools/differential's corpora plus a few more,
184# and left the rest bare rather than record an absence of knowledge as a
185# judgment.
186PARTICLES = NON_GIVEN_NAME_PARTICLES | {
187 'aan',
188 'aen',
189 'abu', # "Abu Bakr" -- Abu reads as the given name; also a bound
190 # given-name word, so it joins forward (#269, P5)
191 'al', # Al Gore: 'Al' is an ordinary English given name. Also the
192 # Arabic definite article, which is the particle reading
193 'bar', # Bar Refaeli; a common modern Israeli given name. Aramaic
194 # "son of" is the particle reading (#269 excludes bare בר)
195 'bat',
196 'bin', # Arabic "son of", but kept ambiguous deliberately: #269
197 # judged the Latin transliteration separately from the
198 # native-script بن, which IS never-given
199 'bon',
200 'da',
201 'dal',
202 'del', # BOTH readings attested: Del Shannon (given name) and
203 # del Toro (particle). The ambiguity flag is the answer
204 'dela', # Dela is a short form of Adela, and an African name
205 # meaning saviour/redeemer -- so the Filipino surname
206 # particle ("Dela Cruz") is not the only reading
207 'della', # Della Reese: an ordinary given name
208 'den',
209 'di', # BOTH: Di as a short form of Diana, and the Italian
210 # particle (DiCaprio)
211 'dí',
212 'do', # Vietnamese Đỗ leads a great many surnames. Also in
213 # TITLES -- decisions.md records TITLES n ambiguous ==
214 # {do, freiherr, st} as load-bearing for the emitter
215 'du', # Du is a Chinese surname (Du Fu), leading under a
216 # family-first reading
217 'freiherr', # German noble title; also in TITLES, load-bearing
218 'freiherrin', # as above
219 'heer',
220 'la', # La Shawn, La Toya: a given-name element. The Romance
221 # article is the particle reading; its PLURALS ('las',
222 # 'los') are never-given, being no one's name
223 'le', # Lê is among the most common Vietnamese surnames and
224 # leads under a family-first reading
225 'mac', # Mac Miller: a real given name. This is the case that
226 # shows #360's vowel heuristic was not the criterion --
227 # 'mac' and 'mc' differ by attestation, not by shape
228 'san', # San San Chiou: a given-name element in Burmese and
229 # Chinese naming
230 'santa',
231 'st', # Also in TITLES and consumed there before particle logic
232 # runs, so moving it changes nothing; kept for the
233 # load-bearing TITLES intersection above
234 'te', # Te Awanui-a-Rangi: a Māori given-name element, and in
235 # the corpus. Looks like the Dutch preposition 'te', which
236 # is why attestation rather than etymology decides
237 'tho',
238 'thoe',
239 'van', # Vietnamese Văn, and Van Johnson -- the canonical
240 # particle-or-given ambiguity this whole flag exists for
241 'vande',
242 'vander',
243 'vel',
244 'von', # Von Miller: a real given name, so this stays ambiguous
245 # even though "von Braun" is misparsed today. The case
246 # that shows a word can look like a pure particle, BE
247 # misparsed as one, and still have a bearer
248
249 # #269: Arabic "abu" (father of), left ambiguous like its Latin
250 # transliteration 'abu' above (both spellings): "Abu Bakr" reads
251 # "Abu" as a given name, so this stays a PARTICLES-only member, not
252 # NON_GIVEN_NAME_PARTICLES.
253 'أبو',
254 'ابو',
255}
256
257# Guard the two invariants the docstring above promises, so a future edit that
258# breaks them fails at import time instead of silently drifting until a test
259# happens to catch it.
260assert NON_GIVEN_NAME_PARTICLES <= PARTICLES, \
261 "NON_GIVEN_NAME_PARTICLES must stay a subset of PARTICLES"
262assert not (NON_GIVEN_NAME_PARTICLES & BOUND_GIVEN_NAMES), \
263 "NON_GIVEN_NAME_PARTICLES must stay disjoint from BOUND_GIVEN_NAMES"
264assert_normalized("PARTICLES", PARTICLES)
265
266# Star imports read __all__ and never the module __getattr__ -- see the
267# note in prefixes.py. This module has no retired name of its own to
268# serve; what __all__ buys here is the other half of that note's point,
269# which the shim modules got in #354 and the destinations did not:
270# without it a star import binds every module-level name, so
271# `assert_normalized` and BOUND_GIVEN_NAMES -- imported only for the
272# invariants below, and owned by another module -- landed in the
273# caller's namespace as though they were vocabulary (#356).
274# Source order, not alphabetical -- see the note in suffixes.py.
275__all__ = ["NON_GIVEN_NAME_PARTICLES", "PARTICLES"]