fix(headings): stop the font census from shredding display-type pages into H1s - #33
Merged
Merged
Conversation
… 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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
On a real 88-page document (Knight Frank Wealth Report 2025),
pdfmux convertpromoted 655 lines to#headings and still reported 97% confidence — the exact silent-garbage failure pdfmux exists to catch, pointed at itself.# CONTACTS firstname./# familyname@/# knightfrank.com# affecting how you live, work,/# invest and give backRoot cause:
_assign_levelspromotes 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, sobody_sizeis 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 likeand/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)
Verification
TestOverInjectionGuards, 8 cases, with real-heading survivors —Introduction,Our contributors,ESG top picks— asserted alongside the demoted false positives)eval/run_eval.pyscores byte-identical to the committed baseline;eval/calibrate.pystill reports precision 1.00 / recall 0.82ruffcleanOut of scope (filed separately)
While reproducing this I found a distinct bug:
--format jsoncollapses all pages into a singlepagesentry when the joined markdown lacks the\n\n---\n\nseparator (formatters/json_fmt.py:56-57), silently dropping the per-page confidence metadata. Not addressed here.🤖 Generated with Claude Code