Description
The prefix-joining loop in join_on_conjunctions (nameparser/parser.py, the # join prefixes to following lastnames section) locates prefix positions with value-based pieces.index(prefix) lookups while mutating pieces in the same loop. When the same prefix word occurs more than once in a chain, .index() re-finds the wrong occurrence and part of the name is silently dropped instead of joined.
Reproduction
>>> from nameparser import HumanName
>>> HumanName("Juan de la de la Vega").last
'de la Vega' # missing the first "de la"
>>> HumanName("Charles van der van der Berg").last
'van der Berg' # missing the first "van der"
Expected:
>>> HumanName("Juan de la de la Vega").last
'de la de la Vega'
>>> HumanName("Charles van der van der Berg").last
'van der van der Berg'
Root cause
prefixes = list(filter(self.is_prefix, pieces)) snapshots prefix values up front. Each loop iteration then does i = pieces.index(prefix) to relocate that prefix — but pieces has already been mutated (merged) by prior iterations, and .index() has no way to distinguish "the occurrence this iteration is supposed to act on" from an earlier occurrence of the same string. The existing except ValueError: pass fallback (reusing a stale i from the previous iteration) works for the common case of exactly two repeated tokens in a chain, but breaks down once a prefix word repeats across what should be two separate joined groups.
This is the same family of bug as #100 (fixed via a targeted one-line pieces.index(stop_at, i + 1) fix elsewhere in this method) — the underlying issue is that the algorithm identifies positions by value instead of tracking them positionally through the mutation.
Proposed fix
Replace the value-based .index() lookup with a single forward scan that tracks position (i/j) directly instead of re-deriving it from string equality, absorbing contiguous prefix runs before searching for the next prefix/suffix boundary. Verified locally against the full test suite (1248 existing tests, zero regressions) plus the two repro cases above (both now correct). Draft implementation and TDD plan: docs/superpowers/plans/2026-07-04-join-on-conjunctions-restructure.md (local, not yet committed/pushed).
Environment
Found via code review while simplifying join_on_conjunctions in PR #207 — not a regression from that PR; reproduces identically on master before and after it.
Description
The prefix-joining loop in
join_on_conjunctions(nameparser/parser.py, the# join prefixes to following lastnamessection) locates prefix positions with value-basedpieces.index(prefix)lookups while mutatingpiecesin the same loop. When the same prefix word occurs more than once in a chain,.index()re-finds the wrong occurrence and part of the name is silently dropped instead of joined.Reproduction
Expected:
Root cause
prefixes = list(filter(self.is_prefix, pieces))snapshots prefix values up front. Each loop iteration then doesi = pieces.index(prefix)to relocate that prefix — butpieceshas already been mutated (merged) by prior iterations, and.index()has no way to distinguish "the occurrence this iteration is supposed to act on" from an earlier occurrence of the same string. The existingexcept ValueError: passfallback (reusing a staleifrom the previous iteration) works for the common case of exactly two repeated tokens in a chain, but breaks down once a prefix word repeats across what should be two separate joined groups.This is the same family of bug as #100 (fixed via a targeted one-line
pieces.index(stop_at, i + 1)fix elsewhere in this method) — the underlying issue is that the algorithm identifies positions by value instead of tracking them positionally through the mutation.Proposed fix
Replace the value-based
.index()lookup with a single forward scan that tracks position (i/j) directly instead of re-deriving it from string equality, absorbing contiguous prefix runs before searching for the next prefix/suffix boundary. Verified locally against the full test suite (1248 existing tests, zero regressions) plus the two repro cases above (both now correct). Draft implementation and TDD plan:docs/superpowers/plans/2026-07-04-join-on-conjunctions-restructure.md(local, not yet committed/pushed).Environment
Found via code review while simplifying
join_on_conjunctionsin PR #207 — not a regression from that PR; reproduces identically onmasterbefore and after it.