Same credential, three spellings, two answers:
parse("田中さん, PhD") # title PhD, family 田中, suffix さん ← peels
parse("田中さん, V.") # family '田中さん', given 'V.' ← does not
parse("田中さん, Ph. D.") # family '田中さん', suffix 'Ph. D.' ← does not
Not a regression — all three are byte-identical to 1.4.0 and to pre-#312 — but #312 fixed the first and left its siblings, so which behavior you get depends on whether the post-comma token passes an initial veto.
Why
#312 scoped the peel's site to the name-bearing runs: segments[:2] under FAMILY_COMMA, segments[0] otherwise. That rests on segments[1] being name text under a family comma. It usually is — but not always.
_segment.py picks SUFFIX_COMMA only when suffixy(groups[1]) and len(groups[0]) > 1. When the post-comma part is entirely suffix-shaped and the pre-comma part is a single word, the structure falls through to FAMILY_COMMA with segments[1] holding post-nominals rather than given-name text. _assign agrees with that reading — it runs _peel_leading_titles over that run and routes it to title/suffix.
So the peel walks into the one kind of run it was scoped away from, and the gap it was scoped against bites:
segment() admits a post-comma run on is_suffix_lenient
_is_post_nominal asks is_suffix_strict
- the initial-shaped suffix words fall between them — measured,
V., V, I are lenient-True and strict-False
An initial-shaped token therefore becomes the peel site, ends in no listed tail, and the peel is silently abandoned. Adding a word before the comma (Dr 田中さん, V.) flips the structure to SUFFIX_COMMA and it peels correctly.
Why it was not fixed in #318
The clean condition is "include segments[1] only when it is actually name text", which is what segment()'s own suffixy already decides. But suffixy is a closure inside segment(), and its comment warns it must stay in sync with group's _PH/_D merge — so a copy in _script_segment would be a third definition of one rule.
The cheap substitute was measured and rejected: all(is_suffix_lenient) fixes V. and Jr. but not Ph. D. (because Ph. alone is not lenient-suffix), leaving PhD and Ph. D. inconsistent — trading one arbitrary split for another.
田中さん, V. is pinned as-is by a parity case row in #318 so the limit is recorded rather than latent.
What the fix probably looks like
Extract suffixy from segment() into _pipeline/_vocab.py alongside is_suffix_lenient/is_suffix_strict, so segment, group and script_segment share one definition of "this run is post-nominals, not name". Then the peel's scope becomes: include the second run under a family comma unless suffixy says it is a suffix run.
Care needed on the Ph./D. merge — suffixy treats an adjacent pair as one unit, and group has a matching merge that the comment says must not drift.
Worth doing alongside anything else that wants the same predicate; it is the third caller that makes the extraction pay.
Found by whole-branch review on #318 (#312).
Same credential, three spellings, two answers:
Not a regression — all three are byte-identical to 1.4.0 and to pre-#312 — but #312 fixed the first and left its siblings, so which behavior you get depends on whether the post-comma token passes an initial veto.
Why
#312 scoped the peel's site to the name-bearing runs:
segments[:2]underFAMILY_COMMA,segments[0]otherwise. That rests onsegments[1]being name text under a family comma. It usually is — but not always._segment.pypicksSUFFIX_COMMAonly whensuffixy(groups[1]) and len(groups[0]) > 1. When the post-comma part is entirely suffix-shaped and the pre-comma part is a single word, the structure falls through toFAMILY_COMMAwithsegments[1]holding post-nominals rather than given-name text._assignagrees with that reading — it runs_peel_leading_titlesover that run and routes it to title/suffix.So the peel walks into the one kind of run it was scoped away from, and the gap it was scoped against bites:
segment()admits a post-comma run onis_suffix_lenient_is_post_nominalasksis_suffix_strictV.,V,Iare lenient-True and strict-FalseAn initial-shaped token therefore becomes the peel site, ends in no listed tail, and the peel is silently abandoned. Adding a word before the comma (
Dr 田中さん, V.) flips the structure toSUFFIX_COMMAand it peels correctly.Why it was not fixed in #318
The clean condition is "include
segments[1]only when it is actually name text", which is whatsegment()'s ownsuffixyalready decides. Butsuffixyis a closure insidesegment(), and its comment warns it must stay in sync withgroup's_PH/_Dmerge — so a copy in_script_segmentwould be a third definition of one rule.The cheap substitute was measured and rejected:
all(is_suffix_lenient)fixesV.andJr.but notPh. D.(becausePh.alone is not lenient-suffix), leavingPhDandPh. D.inconsistent — trading one arbitrary split for another.田中さん, V.is pinned as-is by aparitycase row in #318 so the limit is recorded rather than latent.What the fix probably looks like
Extract
suffixyfromsegment()into_pipeline/_vocab.pyalongsideis_suffix_lenient/is_suffix_strict, sosegment,groupandscript_segmentshare one definition of "this run is post-nominals, not name". Then the peel's scope becomes: include the second run under a family comma unlesssuffixysays it is a suffix run.Care needed on the
Ph./D.merge —suffixytreats an adjacent pair as one unit, andgrouphas a matching merge that the comment says must not drift.Worth doing alongside anything else that wants the same predicate; it is the third caller that makes the extraction pay.
Found by whole-branch review on #318 (#312).