Skip to content

fix(headings): stop the font census from shredding display-type pages into H1s - #33

Merged
NameetP merged 2 commits into
mainfrom
fix/heading-over-injection-display-pages
Aug 13, 2026
Merged

fix(headings): stop the font census from shredding display-type pages into H1s#33
NameetP merged 2 commits into
mainfrom
fix/heading-over-injection-display-pages

Conversation

@NameetP

@NameetP NameetP commented Aug 5, 2026

Copy link
Copy Markdown
Owner

The bug

On a real 88-page document (Knight Frank Wealth Report 2025), pdfmux convert promoted 655 lines to # headings and still reported 97% confidence — the exact silent-garbage failure pdfmux exists to catch, pointed at itself.

  • The masthead rendered as ~16 stacked H1s, splitting a single email across three: # CONTACTS firstname. / # familyname@ / # knightfrank.com
  • Display standfirsts were shredded one-heading-per-wrapped-line: # affecting how you live, work, / # invest and give back

Root cause: _assign_levels promotes any line >= body_size * 1.2, but on a sparse cover/masthead the most-common font by character count is a small caption/legal font, so body_size is under-estimated and nearly every display line clears the bar — with no page-level check that the heuristic had failed.

The fix

Three high-precision, generalizable guards in _clean_false_headings (so both the injection path and the extractor-provided early-exit path are covered — none tuned to specific strings):

  • _looks_like_heading — demote lines that start lowercase or end mid-phrase (trailing , ; : & or a dangling function word like and/to/the). Wrapped prose, not titles.
  • _desaturate_headings — when ≥6 and ≥50% of a page's content lines are headings, the census is unreliable; emit none rather than confident garbage. Clears the masthead entirely.
  • _collapse_heading_runs — a run of ≥5 back-to-back headings with no intervening body prose is a list/table/menu; demote it. Country/asset tables stop rendering one-H1-per-row.

Net: 655 → 379 headings on the report. Masthead, split email, and shredded standfirsts are gone; sub-headings that head real paragraphs (country names over their write-ups) are kept — the run-collapse only fires on prose-free runs.

Before / after (masthead)

BEFORE                        AFTER
# EDITOR Liam Bailey     →    EDITOR Liam Bailey
# familyname@            →    familyname@     (email no longer 3 H1s)
# knightfrank.com        →    knightfrank.com

Verification

  • 767 tests pass (adds TestOverInjectionGuards, 8 cases, with real-heading survivors — Introduction, Our contributors, ESG top picks — asserted alongside the demoted false positives)
  • Zero eval regression: eval/run_eval.py scores byte-identical to the committed baseline; eval/calibrate.py still reports precision 1.00 / recall 0.82
  • ruff clean

Out of scope (filed separately)

While reproducing this I found a distinct bug: --format json collapses all pages into a single pages entry when the joined markdown lacks the \n\n---\n\n separator (formatters/json_fmt.py:56-57), silently dropping the per-page confidence metadata. Not addressed here.

🤖 Generated with Claude Code

… into H1s

On a real 88-page report (Knight Frank Wealth Report 2025) `pdfmux convert`
promoted 655 lines to `#` headings at a reported 97% confidence — the masthead
became ~16 stacked H1s (an email split across three), and display standfirsts
were split one-heading-per-wrapped-line. Root cause: on sparse cover/masthead
pages the most-common-by-char font is a small caption font, so body_size is
under-estimated and nearly every large display line clears the 1.2x heading
bar, with no page-level check that the heuristic failed.

Three generalizable guards in `_clean_false_headings` (cover both the injection
and extractor-provided paths), none tuned to specific strings:

- `_looks_like_heading`: demote lines that start lowercase or end mid-phrase
  (trailing , ; : – & or a dangling function word) — wrapped prose, not titles.
- `_desaturate_headings`: when >=6 and >=50% of a page's content lines are
  headings, the census is unreliable — emit none rather than confident garbage.
- `_collapse_heading_runs`: a run of >=5 back-to-back headings with no body
  prose between is a list/table, demote it.

Net: 655 -> 379 headings on the report; masthead, split email, and shredded
standfirsts gone; sub-headings that head real paragraphs kept. Eval scores
byte-identical to baseline (precision 1.00 / recall 0.82); full suite green.

Adds TestOverInjectionGuards (8 cases) with real-heading survivors asserted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ction-display-pages

# Conflicts:
#	CHANGELOG.md
@NameetP
NameetP merged commit b1cd3c9 into main Aug 13, 2026
8 checks passed
@NameetP
NameetP deleted the fix/heading-over-injection-display-pages branch August 13, 2026 07:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant