Skip to content

Commit 838f949

Browse files
committed
Stop superscript affiliation corrector from grabbing poster body text (0.9.18)
The numbered affiliation list's last entry was bounded only by the end of the scanned banner region, so when the affiliation line and the abstract shared a line (no detected header between them) the last affiliation swallowed the body text (up to ~2000 chars of abstract/section headers on ~108 of 6,725 posters). Block parsing now bails, keeping the corrector a strict no-op, when any parsed affiliation looks like body text: >180 chars, or a run of 5+ consecutive lowercase words (prose) or ALL-CAPS words (section headers). Clean numbered banners are unaffected.
1 parent 161064b commit 838f949

4 files changed

Lines changed: 80 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.9.18] - 2026-06-10
9+
10+
### Fixed
11+
12+
- **The superscript affiliation corrector no longer grabs poster body text as an affiliation.** The numbered affiliation list's last entry was bounded only by the end of the scanned banner region, so when the affiliation line and the abstract shared a line (no detected header between them) the last affiliation swallowed the body text (observed up to 2,119 characters of abstract and section headers across ~108 of 6,725 posters). Block parsing now bails, keeping the corrector a strict no-op, when any parsed affiliation looks like it ran into body text: longer than 180 characters, or containing a run of five or more consecutive lowercase words (prose) or ALL-CAPS words (section headers). Clean numbered banners and the existing correction behavior are unaffected.
13+
814
## [0.9.17] - 2026-06-10
915

1016
### Changed

poster2json/extract.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2002,6 +2002,28 @@ def _is_section_label_only(text: str) -> bool:
20022002
# institution name (optionally space/dot separated, including accented capitals)
20032003
_AFFIL_MARK = re.compile(r"(?:(?<=[\s;,(])|^)(\d{1,2})[.\s]{0,2}(?=[A-ZÀ-ɏ(])")
20042004

2005+
# A parsed affiliation segment that ran past the institution list into the
2006+
# poster body/abstract. The banner region can include body text when the
2007+
# affiliation line and the abstract are not separated by a detected header
2008+
# (e.g. they share a line), and the last numbered entry is delimited only by
2009+
# the end of the region — so it swallows whatever follows. Real affiliations
2010+
# are Title-Case institution/place tokens, not prose: they are not very long
2011+
# and carry no long run of consecutive lowercase or ALL-CAPS words. Such a
2012+
# segment is a strong ambiguity signal, so the corrector bails (no-op).
2013+
_AFFIL_MAX_LEN = 180
2014+
_PROSE_RUN = re.compile(r"\b[a-zà-ÿ]{2,}(?:\s+[a-zà-ÿ]{2,}){4,}\b")
2015+
_CAPS_RUN = re.compile(r"\b[A-Z]{2,}(?:\s+[A-Z]{2,}){4,}\b")
2016+
2017+
2018+
def _affiliation_ran_into_body(txt: str) -> bool:
2019+
"""True if a parsed affiliation segment looks like it ran into poster body
2020+
text (too long, or a sentence-/header-like run of words)."""
2021+
return (
2022+
len(txt) > _AFFIL_MAX_LEN
2023+
or bool(_PROSE_RUN.search(txt))
2024+
or bool(_CAPS_RUN.search(txt))
2025+
)
2026+
20052027

20062028
def _parse_marker_run(run: str):
20072029
"""Parse an author marker run ("1,3" / "1-3,6" / "4 2") into a list of ints,
@@ -2070,7 +2092,7 @@ def _parse_affiliation_block(region: str):
20702092
for j, (num, ms, me) in enumerate(seq):
20712093
end = seq[j + 1][1] if j + 1 < len(seq) else len(region)
20722094
txt = region[me:end].strip().strip(",;").strip()
2073-
if not txt:
2095+
if not txt or _affiliation_ran_into_body(txt):
20742096
return None
20752097
amap[num] = txt
20762098
return amap, seq[0][1]

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[tool.poetry]
22

33
name = "poster2json"
4-
version = "0.9.17"
4+
version = "0.9.18"
55
description = "Convert scientific posters (PDF/images) to structured JSON metadata using Large Language Models"
66

77
packages = [{ include = "poster2json" }]

tests/test_normalize.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,56 @@ def test_superscript_corrector_expands_ranges():
416416
assert len(affs[1]) == 1 and "Beta" in affs[1][0] # Jones 2 -> Beta
417417

418418

419+
def test_superscript_corrector_noop_when_block_runs_into_abstract():
420+
from poster2json.extract import _correct_affiliations_from_superscripts
421+
422+
# The last numbered affiliation is followed immediately by the abstract on
423+
# the same banner run (no header break), so the naive parse would swallow
424+
# the body text. The corrector must detect that and stay a no-op.
425+
raw = (
426+
"Early reproductive failure in kakapo\n"
427+
"Olivia Janes 1, Jana Wold 1,2, Tammy Steeves 2\n"
428+
"1 Centre National de la Recherche Scientifique (CNRS), Rennes, France; "
429+
"2 University of Canterbury, New Zealand "
430+
"Early reproductive failure in kakapo Kakapo are a Nationally Critical "
431+
"taonga species endemic to Aotearoa New Zealand and experience high rates "
432+
"of early embryo death across the breeding population studied here.\n"
433+
)
434+
original = ["School of Biological Sciences"]
435+
result = {"creators": [
436+
{"name": "Janes, Olivia", "familyName": "Janes", "affiliation": list(original)},
437+
{"name": "Wold, Jana", "familyName": "Wold", "affiliation": list(original)},
438+
{"name": "Steeves, Tammy", "familyName": "Steeves", "affiliation": list(original)},
439+
]}
440+
_correct_affiliations_from_superscripts(result, raw)
441+
# Untouched: no reassignment, no abstract text grabbed as an affiliation.
442+
for c in result["creators"]:
443+
assert c["affiliation"] == original
444+
assert not result.get("_validation")
445+
446+
447+
def test_affiliation_ran_into_body_detector():
448+
from poster2json.extract import _affiliation_ran_into_body
449+
450+
assert not _affiliation_ran_into_body("University of California, San Diego, CA, US")
451+
assert not _affiliation_ran_into_body(
452+
"LPL, Laboratoire de Physique des Lasers, Universite Paris 13, "
453+
"Sorbonne Paris Cite, Villetaneuse, France"
454+
)
455+
# prose / abstract text
456+
assert _affiliation_ran_into_body(
457+
"Kakapo are a Nationally Critical taonga species endemic to Aotearoa "
458+
"New Zealand and experience high rates of early embryo death"
459+
)
460+
# all-caps section headers grabbed from the body
461+
assert _affiliation_ran_into_body(
462+
"Univ. of California San Diego USA MOTIVATION THE GAS PUFF Z PINCH "
463+
"THE MACHINE LEARN IMAGE RESULTS"
464+
)
465+
# absurdly long single segment
466+
assert _affiliation_ran_into_body("Institute of " + "x" * 200)
467+
468+
419469
def test_creator_nametype_personal_vs_organizational():
420470
from poster2json.extract import _postprocess_json
421471

0 commit comments

Comments
 (0)