fix(headings): digit-only lines do not define heading tiers (#166)

* fix(headings): digit-only lines do not define heading tiers

A large bold page number (14pt folio over 11pt body) claimed tier 0:
every real heading demoted one level document-wide, and the bold-size
fallback (which requires an empty tier list) was blocked for documents
whose headings match body size.

Bench-neutral (MHS scores relative hierarchy); pdf-evals: 18 docs get
their heading levels back (#### -> ###), semantic composite +0.0006,
no percentile down.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(headings): exclude digit-only lines from the bold fallback tier pass too

The exclusion in the main pass wasn't enough: with the page-number
tier gone, the bold fallback re-collected the same bold folio. Also
regenerates the thermo-freon12 snapshot (cosmetic churn on the
scrambled legend fixture).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Abimael Martell
2026-07-15 13:42:41 -07:00
committed by GitHub
co-authored by Claude Fable 5
parent c908e33b39
commit 0f898a18fb
2 changed files with 28 additions and 3 deletions
+27
View File
@@ -374,6 +374,15 @@ pub(crate) fn compute_heading_tiers(lines: &[TextLine], base_size: f32) -> Vec<f
for line in lines { for line in lines {
if let Some(first) = line.items.first() { if let Some(first) = line.items.first() {
if first.font_size / base_size >= 1.2 { if first.font_size / base_size >= 1.2 {
// Digit-only lines (page numbers, issue numbers) must not
// define heading tiers: a large bold folio claims tier 0 and
// blocks the bold-size fallback for the document's real
// same-size headings.
let text = line.text();
let t = text.trim();
if !t.is_empty() && t.chars().all(|c| !c.is_alphabetic()) {
continue;
}
heading_sizes.push(first.font_size); heading_sizes.push(first.font_size);
} }
} }
@@ -398,6 +407,11 @@ pub(crate) fn compute_heading_tiers(lines: &[TextLine], base_size: f32) -> Vec<f
if tiers.is_empty() { if tiers.is_empty() {
let mut bold_sizes: Vec<f32> = lines let mut bold_sizes: Vec<f32> = lines
.iter() .iter()
.filter(|line| {
let text = line.text();
let t = text.trim();
!t.is_empty() && t.chars().any(|c| c.is_alphabetic())
})
.filter_map(|line| line.items.first()) .filter_map(|line| line.items.first())
.filter(|it| it.is_bold && it.font_size / base_size >= 1.05) .filter(|it| it.is_bold && it.font_size / base_size >= 1.05)
.map(|it| it.font_size) .map(|it| it.font_size)
@@ -508,6 +522,19 @@ mod tests {
} }
} }
#[test]
fn digit_only_lines_do_not_define_tiers() {
// A 14pt bold page number must not claim tier 0 — that both demotes
// every real heading a level and blocks the bold-size fallback.
let lines = vec![
line_of("76", 14.0, true, 760.0),
line_of("Replace", 11.0, true, 700.0),
line_of("body text at eleven points", 11.0, false, 680.0),
];
let tiers = compute_heading_tiers(&lines, 11.0);
assert!(tiers.is_empty(), "page number claimed a tier: {tiers:?}");
}
#[test] #[test]
fn bold_fallback_tiers_when_nothing_clears_ratio_gate() { fn bold_fallback_tiers_when_nothing_clears_ratio_gate() {
// 10pt body, 11pt bold section headings (book-style): no size clears // 10pt body, 11pt bold section headings (book-style): no size clears
+1 -3
View File
@@ -6,9 +6,7 @@
#### Thermodynamic Properties #### Thermodynamic Properties
**of** **of** ®
®
# Freon 12 # Freon 12