From 3e14ea20dc4385ea295ed61dc7e6ec0ab2403187 Mon Sep 17 00:00:00 2001 From: Derek Gulbranson Date: Wed, 1 Jul 2026 02:44:29 -0700 Subject: [PATCH 1/2] Add German/Dutch prefixes and German title/degree suffixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #18. Adds prefixes (aan, aen, auf, dem, freiherr, freiherrin, heer, het, op, te, tho, thoe, vande, vd) and titles/suffixes (Dipl.-Ing., FH-Prof., Gräfin, Me., PD, Priv.-Doz., RA, Univ.Prof., WP, ba, bsc, meng, stb, MdB/MdL/MdEP/MdA/MdHB/MdBB) that don't collide with existing English-language parsing. Also fixes join_on_conjunctions() to register a conjunction-merged piece (e.g. "von" + "und" + "zu") as a prefix too, mirroring the existing title-handling, so multi-word prefix chains like German "von und zu" bridge correctly into the last name instead of getting stranded in the middle name. Deliberately left out short, high-frequency English words (to, in, an, then, ten) that collide with common Korean/Vietnamese given-name syllables in the middle-token position, and bare "v" as a prefix, which collides with ordinary Western middle initials. Co-Authored-By: Claude Sonnet 5 --- nameparser/config/prefixes.py | 14 ++++++++++++++ nameparser/config/suffixes.py | 10 ++++++++++ nameparser/config/titles.py | 9 +++++++++ nameparser/parser.py | 10 ++++++++++ tests/test_conjunctions.py | 31 +++++++++++++++++++++++++++++++ 5 files changed, 74 insertions(+) diff --git a/nameparser/config/prefixes.py b/nameparser/config/prefixes.py index 1d36ce1..ee1c0b4 100644 --- a/nameparser/config/prefixes.py +++ b/nameparser/config/prefixes.py @@ -9,9 +9,12 @@ #: correct parsing of the last name "von bergen wessels". PREFIXES = set([ "'t", + 'aan', + 'aen', 'abu', 'af', 'al', + 'auf', 'av', 'bar', 'bat', @@ -30,6 +33,7 @@ 'delle', 'delli', 'dello', + 'dem', 'den', 'der', 'di', @@ -37,18 +41,28 @@ 'do', 'dos', 'du', + 'freiherr', + 'freiherrin', + 'heer', + 'het', 'ibn', 'la', 'le', 'mac', 'mc', + 'op', 'san', 'santa', 'st', 'ste', + 'te', 'ter', + 'tho', + 'thoe', 'van', + 'vande', 'vander', + 'vd', 'vel', 'vom', 'von', diff --git a/nameparser/config/suffixes.py b/nameparser/config/suffixes.py index e32bc8f..ec83f68 100644 --- a/nameparser/config/suffixes.py +++ b/nameparser/config/suffixes.py @@ -97,6 +97,7 @@ 'asp', 'atc', 'awb', + 'ba', 'bca', 'bcl', 'bcss', @@ -107,6 +108,7 @@ 'bpe', 'bpi', 'bpt', + 'bsc', 'bt', 'btcs', 'bts', @@ -507,8 +509,15 @@ 'mcse', 'mct', 'md', + 'mda', + 'mdb', + 'mdbb', + 'mdep', + 'mdhb', 'mdiv', + 'mdl', 'mem', + 'meng', 'mfa', 'micp', 'mieee', @@ -653,6 +662,7 @@ 'sphr', 'sra', 'sscp', + 'stb', 'stmieee', 'tbr-ct', 'td', diff --git a/nameparser/config/titles.py b/nameparser/config/titles.py index bfa90fe..6fdec40 100644 --- a/nameparser/config/titles.py +++ b/nameparser/config/titles.py @@ -231,6 +231,7 @@ 'detective', 'developer', 'dhr', + 'dipl.-ing', 'diplomat', 'dir', 'director', @@ -282,6 +283,7 @@ 'fadm', 'family', 'federal', + 'fh-prof', 'field', 'film', 'financial', @@ -312,6 +314,7 @@ 'goodwife', 'governor', 'graf', + 'gräfin', 'grand', 'großfürst', 'group', @@ -412,6 +415,7 @@ 'mcpoc', 'mcpon', 'md', + 'me', 'member', 'memoirist', 'merchant', @@ -470,6 +474,7 @@ 'paleontologist', 'pastor', 'patriarch', + 'pd', 'pediatrician', 'personality', 'petty', @@ -512,6 +517,7 @@ 'printer', 'printmaker', 'prinz', + 'priv.-doz', 'prior', 'private', 'pro', @@ -526,6 +532,7 @@ 'pursuivant', 'pv2', 'pvt', + 'ra', 'rabbi', 'radio', 'radm', @@ -648,6 +655,7 @@ 'tsgt', 'uk', 'united', + 'univ.prof', 'us', 'vadm', 'vardapet', @@ -665,6 +673,7 @@ 'warrant', 'wing', 'wm', + 'wp', 'wo-1', 'wo1', 'wo2', diff --git a/nameparser/parser.py b/nameparser/parser.py index f36e9f1..7b2e83f 100644 --- a/nameparser/parser.py +++ b/nameparser/parser.py @@ -1149,6 +1149,11 @@ def join_on_conjunctions(self, pieces: list[str], additional_parts_count: int = if self.is_title(pieces[i+1]): # when joining to a title, make new_piece a title too self.C.titles.add(new_piece) + if self.is_prefix(pieces[i+1]): + # when joining to a prefix, make new_piece a prefix too, so + # e.g. "von" + "und" bridges into "von und" and can still + # chain onto a following prefix/lastname (see "von und zu") + self.C.prefixes.add(new_piece) pieces[i] = new_piece pieces.pop(i+1) # subtract 1 from the index of all the remaining conjunctions @@ -1161,6 +1166,11 @@ def join_on_conjunctions(self, pieces: list[str], additional_parts_count: int = if self.is_title(pieces[i-1]): # when joining to a title, make new_piece a title too self.C.titles.add(new_piece) + if self.is_prefix(pieces[i-1]): + # when joining to a prefix, make new_piece a prefix too, so + # e.g. "von" + "und" bridges into "von und" and can still + # chain onto a following prefix/lastname (see "von und zu") + self.C.prefixes.add(new_piece) pieces[i-1] = new_piece pieces.pop(i) rm_count = 2 diff --git a/tests/test_conjunctions.py b/tests/test_conjunctions.py index 94215c3..ab50bb9 100644 --- a/tests/test_conjunctions.py +++ b/tests/test_conjunctions.py @@ -223,3 +223,34 @@ def test_conjunction_in_an_address_with_a_first_name_title(self) -> None: def test_name_is_conjunctions(self) -> None: hn = HumanName("e and e") self.m(hn.first, "e and e", hn) + + def test_conjunction_bridges_prefix_chain(self) -> None: + # "von" and "zu" are both prefixes, but "und" between them is only a + # conjunction. join_on_conjunctions() merges "von und zu" into one + # piece before the prefix-joining step runs, so without registering + # that merged piece as a prefix too, it's stranded in the middle name + # instead of joining to the last name. See German nobility styles + # like "von und zu". + hn = HumanName("Alois von und zu Liechtenstein") + self.m(hn.first, "Alois", hn) + self.m(hn.middle, "", hn) + self.m(hn.last, "von und zu Liechtenstein", hn) + + def test_conjunction_bridges_prefix_chain_with_leading_title(self) -> None: + # Same bridging, but with extra prefix words on both sides of the + # conjunction and a leading title-like word ("Freiherrin") that is + # itself a prefix, confirming the chain still joins fully into last. + hn = HumanName("Annette Charlotte Freiherrin von und zu der Tann-Rathsamhausen") + self.m(hn.first, "Annette", hn) + self.m(hn.middle, "Charlotte", hn) + self.m(hn.last, "Freiherrin von und zu der Tann-Rathsamhausen", hn) + + def test_conjunction_prefix_merge_at_start_stays_first_name(self) -> None: + # Guards the i == 0 branch of the same fix: when the conjunction is + # merged with a following prefix at the very start of the name, the + # existing leading-prefix rule (a lone prefix opening the name is + # treated as part of the first name, not joined to last) must still + # apply to the merged piece. + hn = HumanName("and van Buren") + self.m(hn.first, "and van", hn) + self.m(hn.last, "Buren", hn) From e739655059591aa83e2d1663e4621059503e56d7 Mon Sep 17 00:00:00 2001 From: Derek Gulbranson Date: Wed, 1 Jul 2026 02:51:19 -0700 Subject: [PATCH 2/2] Add tests for dual title+prefix and multi-conjunction chains Covers two gaps flagged in review: a merged piece that's registered as both a title and a prefix ("freiherr"), and a chain with more than one non-contiguous conjunction bridging prefixes into the last name. --- tests/test_conjunctions.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/test_conjunctions.py b/tests/test_conjunctions.py index ab50bb9..05b27de 100644 --- a/tests/test_conjunctions.py +++ b/tests/test_conjunctions.py @@ -254,3 +254,23 @@ def test_conjunction_prefix_merge_at_start_stays_first_name(self) -> None: hn = HumanName("and van Buren") self.m(hn.first, "and van", hn) self.m(hn.last, "Buren", hn) + + def test_conjunction_bridges_word_that_is_both_title_and_prefix(self) -> None: + # "freiherr" is registered as both a title and a prefix. When it sits + # next to a conjunction, join_on_conjunctions() runs the is_title and + # is_prefix checks independently (not elif), so the merged piece + # ("freiherr und") is added to both constants sets. Confirms that + # dual registration doesn't break the prefix-bridging into last. + hn = HumanName("Fritz Freiherr und von Bar") + self.m(hn.first, "Fritz", hn) + self.m(hn.middle, "", hn) + self.m(hn.last, "Freiherr und von Bar", hn) + + def test_conjunction_bridges_prefix_chain_with_multiple_conjunctions(self) -> None: + # Two separate conjunctions ("und" appearing twice, not contiguous) + # each bridge their own pair of adjacent prefixes, so both merges + # must register as prefixes for the whole chain to join into last. + hn = HumanName("Alois von und zu und von Liechtenstein") + self.m(hn.first, "Alois", hn) + self.m(hn.middle, "", hn) + self.m(hn.last, "von und zu und von Liechtenstein", hn)