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
20 changes: 14 additions & 6 deletions AGENTS.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ authors:
repository-code: "https://github.com/derek73/python-nameparser"
url: "https://github.com/derek73/python-nameparser"
license: LGPL-2.1-or-later
version: "2.6.0"
version: "2.7.0"
13 changes: 8 additions & 5 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ nameparser parses human names into seven fields — title, given, middle,
family, suffix, nickname, maiden. Results are immutable, configuration is
composable, and locale packs are opt-in.

📣 **nameparser 2.6 is out.** It keeps surnames that earlier releases
read as credentials (``Luo Si``, ``Maria de Sa``, ``Ram Bahadur B.K.``),
reads ``Wm`` and ``Md`` as the given names they abbreviate, recognizes more credentials after a comma
(``John Smith, MBChB``), and adds an opt-in reading for names with two
family names (``Gabriel García Márquez``). The `release log
📣 **nameparser 2.7 is out.** It is the first release working towards
the new language and vocabulary model described in the `vocabulary
model RFC <https://github.com/derek73/python-nameparser/blob/master/docs/design/vocabulary-model-rfc.md>`__.
It introduces more nuanced handling of titles and particles
(``Chief Justice John Roberts`` reads the title ``Chief Justice``,
while ``Justice Smith`` keeps the given name ``Justice``), adjusts the
default vocabulary to better align with example name corpora, and
surfaces alternative name readings in the published API. The `release log
<https://nameparser.readthedocs.io/en/latest/release_log.html>`__ has
the details.

Expand Down
4 changes: 3 additions & 1 deletion docs/concepts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ work. A vocabulary layer runs first and
claims words for what they *are*, wherever they sit: titles, particles,
conjunctions, recognized suffixes, and anything set off by nickname or
maiden delimiters. Titles chain, so ``"Asst. Vice Chancellor"`` is one
title; particles join forward, so ``de la`` attaches to ``Vega``.
title, and a title that is also a given name, such as ``Dean``, is
claimed only inside such a phrase (``"Dean of Students"``); particles
join forward, so ``de la`` attaches to ``Vega``.

Whatever the vocabulary layer has not claimed is left to a positional
layer, which assigns purely by where a word sits: the first unclaimed
Expand Down
236 changes: 172 additions & 64 deletions docs/customize.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,17 @@ Adding and removing words

.. doctest::

>>> from nameparser import Lexicon, Parser
>>> from nameparser import Lexicon, Parser, parse
>>> parse("Dean Robert Johns").given
'Dean'
>>> lex = Lexicon.default().add(titles={"dean"})
>>> Parser(lexicon=lex).parse("Dean Robert Johns").title
'Dean'

By default ``Dean`` is a title only inside a title phrase such as
``Dean of Students`` (:ref:`titles-also-given-names`); listing it in
``titles`` makes it a title wherever it stands.

:meth:`~nameparser.Lexicon.add` and :meth:`~nameparser.Lexicon.remove`
both return a new :class:`~nameparser.Lexicon` — the one you started
from (here, ``Lexicon.default()``) is never mutated. Every field
Expand Down Expand Up @@ -102,41 +108,55 @@ Removing works the same way, and drops the word from recognition:
>>> Parser(lexicon=lean).parse("Professor Robert Johns").title
''

A few fields mark a subset of another — ``given_name_titles``,
``after_given_titles``, ``title_modifiers`` and ``title_styles`` over
``titles``, ``particles_ambiguous`` over ``particles``,
``suffix_acronyms_ambiguous`` over ``suffix_acronyms``,
``conjunctions_ambiguous`` over ``conjunctions``,
``honorific_tails`` and ``suffix_words_ambiguous`` over
``suffix_words``, and ``agnomens`` over ``suffix_words_ambiguous``.
Entries belong in the base field too, so add to both and remove from
the marker first. Four of them enforce that — ``particles_ambiguous``,
``suffix_acronyms_ambiguous``, ``honorific_tails`` and
``suffix_words_ambiguous`` raise ``ValueError`` naming the orphans,
because an orphan in each of those does real harm rather than nothing.
The others are deliberately unchecked because an orphan there is
inert, an ``agnomens`` entry being read only for a word
``suffix_words_ambiguous`` marks, and a ``title_modifiers`` or
``title_styles`` entry only for a title:
``given_name_titles`` matches a title run as one space-joined string, or
by that run's last word, so a legitimate entry like ``"sir and dame"``
is no single word in ``titles``; and a ``conjunctions_ambiguous`` or
``after_given_titles`` entry is only ever read for a word that is a
conjunction or a title, so ``remove(conjunctions={"e"})`` simply works
and the stale marker entry is never consulted.

``titles_ambiguous`` is the one title field that is no subset of
``titles``: a word in it is a title only inside a title phrase, and a
word in both is a title wherever it stands, which is what listing it in
``titles`` asks. ``title_phrases`` holds the two-word phrases a
``titles_ambiguous`` word may stand in (``"chief justice"``,
``"major general"``), each read only where both words are titles:
Marking a subset of another field
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. doctest::
Several fields mark a subset of another, qualifying how the base
field's words are read. Their entries belong in the base field too, so
add a word to both, and remove it from the marker first. An entry
outside its base is an *orphan*: four markers refuse one with a
``ValueError`` naming it, because there it would change the parse; the
rest leave it unchecked, because there it is never read.

>>> chief = Lexicon.default().remove(title_phrases={"chief justice"})
>>> Parser(lexicon=chief).parse("Chief Justice John Roberts").title
'Chief'
.. list-table::
:header-rows: 1
:widths: 30 34 36

* - Marker
- Base
- An orphan
* - ``particles_ambiguous``
- ``particles``
- raises ``ValueError``
* - ``suffix_acronyms_ambiguous``
- ``suffix_acronyms``
- raises ``ValueError``
* - ``honorific_tails``
- ``suffix_words``
- raises ``ValueError``
* - ``suffix_words_ambiguous``
- ``suffix_words``
- raises ``ValueError``
* - ``agnomens``
- ``suffix_words_ambiguous``
- is never read
* - ``conjunctions_ambiguous``
- ``conjunctions``
- is never read
* - ``after_given_titles``
- ``titles``
- is never read
* - ``given_name_titles``, ``title_modifiers``, ``title_styles``
- ``titles`` or ``titles_ambiguous``
- is never read

So ``remove(conjunctions={"e"})`` simply works, and the stale
``conjunctions_ambiguous`` entry is never consulted.
``given_name_titles`` is unchecked for a second reason: it matches a
run of titles as one space-joined string, or by the run's last word, so
a legitimate entry like ``"sir and dame"`` is no single word of
``titles``. ``titles_ambiguous`` is no subset of ``titles`` at all; see
:ref:`titles-also-given-names`.

.. _title-detection-off:

Expand Down Expand Up @@ -332,7 +352,9 @@ them — ``suffix_acronyms_ambiguous``, ``suffix_words_ambiguous``,
``suffix_acronyms``, ``suffix_words``, ``particles`` and
``conjunctions``. They add no vocabulary by themselves; they narrow how
an existing entry is read when it appears alone, and the parse reports
the fork as an ambiguity.
the fork as an ambiguity. Titles that are also given names work
differently, with a word list of their own, ``titles_ambiguous``
(:ref:`titles-also-given-names`).

Credentials that are also surnames
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Expand Down Expand Up @@ -432,12 +454,67 @@ free:
needs one adds it back with
``Lexicon.default().add(suffix_acronyms={"cha"})``.

The same conservatism is why ``dean`` above is a title by default only
inside a title phrase ("Dean of Students", "Associate Dean"): "Dean" is
also a common given name, and a default that swallowed it as a title
would misparse "Dean Martin" for everyone. Listing it in ``titles``
makes it a title wherever it stands. The phrase-only titles are
``Lexicon.titles_ambiguous``.
.. _titles-also-given-names:

Titles that are also given names
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Many titles are borne as given names too: ``Dean``, ``King``,
``Prince``, ``Grace``, ``Major``, ``Sheikh``. A title addresses the
name after it, so at the front of a name there is no count of words to
read, and what tells the person ``Dean Smith`` from the office in
``Dean of Students`` is the phrase the word stands in. Since 2.7 these
words are listed in ``titles_ambiguous`` rather than ``titles``, and
each is a title only inside a title phrase: behind a style
(``His Majesty King``) or a modifier (``Associate Dean``), before
``of`` (``Dean of Students``), or in a listed two-word phrase
(``Major General``). Anywhere else it is a name word:

.. doctest::

>>> parse("Dean Smith").given
'Dean'
>>> parse("Associate Dean Smith").title
'Associate Dean'
>>> parse("Dean of Students Jane Smith").title
'Dean of Students'
>>> parse("Major General John Smith").title
'Major General'

Where such a word opens the name and a listed phrase could have held
it, the parse reports a ``title-or-name`` ambiguity, its alternative
the title reading (:ref:`reading-not-taken`). A word no listed phrase
holds, such as ``Grace`` (a title in ``His Grace``), reports nothing
standing alone:

.. doctest::

>>> dean_alt = parse("Dean Smith").ambiguities[0]
>>> dean_alt.kind.value, dean_alt.alternative.kind.value
('title-or-name', 'title')
>>> parse("Grace Smith").ambiguities
()

Four fields hold this vocabulary:

- ``titles_ambiguous`` — the titles read only inside a phrase. A
word listed in ``titles`` as well is a title wherever it stands, so
``add(titles={"dean"})`` (the first example on this page) is how to
read one as a title everywhere.
- ``title_modifiers`` — the words that qualify a head title
(``associate``, ``vice``).
- ``title_styles`` — the forms of address a title may stand behind
(``majesty``, ``highness``, ``excellency``).
- ``title_phrases`` — the two-word phrases, each read only where
both words are titles. Removing one reads its words apart:

.. doctest::

>>> parse("Chief Justice John Roberts").title
'Chief Justice'
>>> chief = Lexicon.default().remove(title_phrases={"chief justice"})
>>> Parser(lexicon=chief).parse("Chief Justice John Roberts").title
'Chief'

Particles that are also given names
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Expand Down Expand Up @@ -588,13 +665,10 @@ listed below.
- Effect
* - ``name_order``
- one of the three exported order constants
- Assigns positional (no-comma) input to given/middle/family in
this order. Use the exported ``GIVEN_FIRST`` (default),
``FAMILY_FIRST``, or ``FAMILY_FIRST_GIVEN_LAST`` constants.
Ignored when a comma separates family from given ("Thomas,
John" puts the family name first); a comma that only sets off
suffixes ("John Smith, Jr.") leaves it governing the name part.
See :ref:`name-order`.
- Assigns positional input to given/middle/family in this order:
``GIVEN_FIRST`` (default), ``FAMILY_FIRST`` or
``FAMILY_FIRST_GIVEN_LAST``. A family comma overrides it. See
:ref:`name-order`.
* - ``script_orders``
- pairs of ``Script`` and an order
- Assigns a name in one of these scripts in the paired order,
Expand Down Expand Up @@ -713,6 +787,14 @@ family-then-given regardless of the configured order:
>>> family_first.parse("Thomas, John").family
'Thomas'

A comma that only sets off suffixes leaves the order governing the
name before it:

.. doctest::

>>> family_first.parse("Smith John, Jr.").given
'John'

A Vietnamese full name needs a third order. It is written family, then
middle, then given — the name a person is actually called by is the
*last* word, not the second. Family-first order gets the family name
Expand Down Expand Up @@ -1175,18 +1257,26 @@ instead of extending them, the same trap as ``capitalization_exceptions``:
Suffixes not separated by commas
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

A semicolon separates post-nominals as a comma does, with no
configuration. Glued to the word before it, it stays in the suffix as
written:
A comma is the usual separator between post-nominals, but sources write
others. A few are read with no configuration, any other can be
declared, and a credential joined to another, or split by a space,
needs neither.

Separators read without configuration
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

A semicolon separates post-nominals as a comma does. Glued to the word
before it, it stays in the suffix as written:

.. doctest::

>>> name = parse("Jane Smith, RN; CRNA")
>>> name.given, name.family, name.suffix
('Jane', 'Smith', 'RN; CRNA')

So does a hyphen or a slash standing alone after the comma (since 2.7),
which is dropped as a comma between suffixes would be:
So does a hyphen or a slash standing alone, with a space on each side,
after a whole name and its comma or after a further comma (since 2.7).
It is dropped, as a comma between suffixes would be:

.. doctest::

Expand All @@ -1197,19 +1287,21 @@ which is dropped as a comma between suffixes would be:
'Jane'

Glued to a word, a hyphen or slash is the word's own, so
``Jane Smith, RN- CRNA`` reads as it always has, with ``RN-`` as the given
name and ``Jane Smith`` as the family; declare ``" - "`` to read the word
without its glued hyphen, as the semicolon is read. Behind a single word (``Smith, RN - CRNA``)
nothing is split, and before the comma it is a word too, so it never reaches a
hyphenated surname written with spaces (``Smith - Jones``).
``Jane Smith, RN- CRNA`` reads as it always has, with ``RN-`` as the
given name and ``Jane Smith`` as the family. Behind a single word
(``Smith, RN - CRNA``) nothing is split, and before the comma the
hyphen is a word too, so it never reaches a hyphenated surname written
with spaces (``Smith - Jones``).

Declaring another separator
^^^^^^^^^^^^^^^^^^^^^^^^^^^

``extra_suffix_delimiters`` handles sources that separate post-nominals
with something else. Undeclared, the separator is a word.
Since 2.4 such a name still parses when a credential opens the part
after the comma, because that makes the whole part the suffix, but the
separator stays in the suffix as written. Declared, it splits the
suffix into groups the way a comma does, and it counts glued to a word
as the semicolon does:
with something else. Undeclared, the separator is a word. Since 2.4
such a name still parses when a credential opens the part after the
comma, because that makes the whole part the suffix, but the separator
stays in the suffix as written. Declared, it splits the suffix into
groups the way a comma does:

.. doctest::

Expand All @@ -1221,11 +1313,18 @@ as the semicolon does:
>>> name.given, name.family, name.suffix
('Jane', 'Smith', 'RN, CRNA')

A declared separator also counts glued to a word, as the semicolon
does, and stays in the suffix as written there: declaring ``" - "``
reads ``Jane Smith, RN- CRNA`` as given ``Jane``, family ``Smith``.

Through 2.3 the undeclared reading of ``Jane Smith, RN - CRNA`` was
given ``RN``, family ``Jane Smith`` and suffix ``CRNA``, so the
delimiter was the only way to get the name right, and through 2.6 it
was the only way to split the suffix.

Credentials written joined or split
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

A hyphen or a slash written between two credentials with no space needs
no declaring when every part is a listed credential that is not also a
name, because the word then reads as one credential (since 2.7). A part
Expand All @@ -1242,6 +1341,15 @@ which :ref:`all-caps-credentials` reads on its own terms:
>>> joined.given, joined.family
('MA/PhD', 'Jane Smith')

A credential written with a space inside it, such as ``Pharm D``, is
read as one credential too (since 2.7), where the name has words to
spare in front of it:

.. doctest::

>>> parse("John Smith, Pharm D").suffix
'Pharm D'

.. _unlisted-credentials:

Credentials the vocabulary doesn't list
Expand Down
2 changes: 1 addition & 1 deletion docs/design/decisions/H5-trailing-title-run.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
- Transparency holds for one name word where a title run stands in front of it (`Sir John Prof.` → given 'John'); with none in front, the trailing title decides that word's field as H1 reads it (`Smith Prof.` → family 'Smith', `Smith Sir.` → given 'Smith') — 2026-09-09 (review round 2).
- On a main segment the particle chain stops in front of a trailing title (`John van der Berg Prof.` → title 'Prof.', family 'van der Berg'), and a title a connective joined into a longer unit is no single word (`John Smith Prof. and Dr.` → family 'Prof. and Dr.') — 2026-10-07 #614.
- After a family comma the given part's chain stops where `tail_reading`'s run begins, and P6 reads the particle as though the title were absent: `Smith, John van der Berg Prof.` → middle 'van der Berg', `Smith, Jan van Prof.` → family 'van Smith' — 2026-10-10 #623.
- Where that bound and the given part's own walk disagree behind a title, garbage shapes read off it, recorded and not fixed (`Smith, John Mc Prof. VI` → suffix 'Mc', middle 'Prof. VI') — 2026-10-10 #623.
- Where that bound and the given part's own walk disagree behind a title, garbage shapes read off it, recorded and not fixed (`Smith, John Mc Prof. V, Jr.` → suffix 'Mc, Jr.', middle 'Prof. V'; the entry's `Smith, John Mc Prof. VI` has read title 'Prof.', suffix 'VI' since #784) — 2026-10-10 #623.

Elsewhere: a maiden clause in front of a trailing title is no join that hides it (`Mary Smith née Jones Prof.` → title 'Prof.'), decisions/M2's 2026-09-26 #535 entry; a bare ambiguous acronym ending the given part after a family comma is the credential, so `Doe, John Prof. MA` reads title 'Prof.', suffix 'MA', decisions/S5's 2026-09-18 #531 entry on the comma report's reach; the bound join after a family comma declines a title word this run takes (`Berg, abdul Prof.` → title 'Prof.'), decisions/P5's 2026-10-11 #637 entry; a part past the second comma makes no particle chain, so `John Smith, Jr., van der Berg Prof.` reads title 'Prof.', decisions/C7's 2026-10-11 #636 entry; a report names the field its word lands in, closing the `Dr. John of Prince` detail mismatch, decisions/A1's 2026-10-08 #626 entry; the cost of a period behind an ordinary surname that is title vocabulary (`Mary Jane King.` → title 'King.', family 'Jane') is the trailing-position bullet of decisions/vocabulary-exclusions.

Expand Down
2 changes: 1 addition & 1 deletion docs/design/decisions/generational-words.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Elsewhere: the run start that lets any unambiguous suffix word of two or more le
- pt `júnior`, the accented spelling of `junior`, ships beside it with `same_as = "jr"` and inherits its vetting and its accepted collisions rather than being re-vetted under the two tests above (Junior is a borne surname, 2,146 US Census bearers, where `senior` was refused for 3,864): `José Silva Júnior` reads family 'Silva', suffix 'Júnior', as `José Silva Junior` does — 2026-10-23 (Derek), #843.
- The Brazilian agnomens `Filho`, `Neto`, `Netto`, `Sobrinho` and their feminines are generation readings of the `agnomen` kind, gated by the count alone (rules/S11): `Pedro Alves Filho` reads family 'Alves', suffix 'Filho', and `Ana Neto` keeps family 'Neto', both reported — 2026-10-25 #752.

Open: #684 (locale packs for `Jiří Dienstbier ml.` and `st.`, the generational words that collide outside their language); #843 (the agnomens' abbreviations FH, FO, NT and SB).
Open: #684 (locale packs for `Jiří Dienstbier ml.` and `st.`, the generational words that collide outside their language). The agnomens' abbreviations (#843) are decided in decisions/S11 (2026-10-27).

## Log

Expand Down
Loading
Loading