9797| ` output.rs ` | ` dedup_heading_echo() ` | Both (runs in reflow) |
9898| ` output.rs ` | ` dedup_consecutive_text() ` | Both (runs in reflow) |
9999
100+ ## Text-Only Detection Logic
101+
102+ ### Font-based heading detection
103+ The ** font is the definitive signal** for headings. ` partition_heading_chars() `
104+ extracts chars matching the heading font family (e.g., LinBiolinum vs body
105+ font LinLibertine). Heading chars are validated against ` extract_headings() `
106+ results. Known headings take ** priority over formula zones** — a heading at
107+ a Y-position overlapping a formula zone is kept because font overrides
108+ geometric heuristics.
109+
110+ Body blocks should NEVER be classified as headings when font-based detection
111+ is active (` has_font_headings ` flag). Multi-line body blocks that happen to
112+ start with heading text are not headings — the font is the tell.
113+
114+ ### Formula detection
115+ Two detection paths at the line level:
116+ 1 . ** Content-based** (` is_likely_formula_text() ` ) — operators, math symbols,
117+ prose word rejection, ` $...$ ` marker density
118+ 2 . ** Font-based** — ≥3 math italic Unicode chars (U+1D400-1D7FF) with no
119+ prose words. These chars ARE the font signal.
120+
121+ Formula detection is ** suppressed in algorithm zones** (see below).
122+
123+ ** Key issues encountered:**
124+ - ` $ ` /` { ` /` } ` /` _ ` /` ^ ` markers from ` extract_region_text() ` inflate char
125+ counts and dilute math_ratio. Use ` content_total ` (excluding these
126+ formatting chars) for ratio calculations.
127+ - "fi"/"fifi" ligature artifacts from PDF absolute value bars ` |...| ` look
128+ like prose words. Excluded via ` is_ligature_artifact() ` .
129+ - Pseudocode keywords ("if", "for") should only reject formulas when at the
130+ START of a line (pseudocode), not mid-line (piecewise formula conditions
131+ like "value, if condition").
132+ - No upper char limit on ` is_likely_formula_text() ` — piecewise formulas
133+ and multi-line formulas can be very long.
134+
135+ ### Algorithm detection
136+ Algorithm zones are detected from ** line numbers** ("1:", "15:", "37:") — an
137+ unambiguous structural signal that display formulas never have. When ≥2
138+ numbered lines exist in a column, their Y-range defines an algorithm zone.
139+
140+ Inside algorithm zones:
141+ - ** Formula detection suppressed** — pseudocode with math variables is not
142+ a display formula
143+ - ** All heuristic breaks suppressed** (y_break, x_break, font_break) —
144+ algorithm pseudocode has subscript fragments, varying indentation, and
145+ font size changes that would fragment the block
146+
147+ Algorithm caption splitting: when a block starts with "Algorithm N * title* "
148+ and contains numbered lines, the caption is split into a separate
149+ ` FigureTitle ` region and the body becomes an ` Algorithm ` region.
150+
151+ In the reflow stage, ` Algorithm ` nodes are ** never demoted to Text** .
152+ They either stay as ` Algorithm ` (pseudocode) or get promoted to ` CodeBlock `
153+ (actual programming code). ` Algorithm ` renders as plain text (not fenced
154+ code blocks) because algorithms can contain ` $...$ ` LaTeX math.
155+
156+ ### Subscript / superscript handling
157+ ** Fundamental issue** : PDF text layer chars in math expressions span
158+ multiple Y positions (subscripts, superscripts, fraction numerators and
159+ denominators). ` group_into_lines() ` uses Y-proximity to group chars into
160+ lines.
161+
162+ ** Current approach** : bbox-based line grouping. A char belongs to the current
163+ line if its Y-center falls within the line's Y bounding box (expanded by
164+ ` avg_height * 0.3 ` padding). This handles normal subscripts (3-4pt offset)
165+ but NOT fraction numerators/denominators (6-8pt offset) because they would
166+ merge actual separate prose lines.
167+
168+ ** Remaining limitation** : fraction parts (` 1/Δt² ` ) still fragment into
169+ separate lines in the text-only path. The ML layout path handles this via
170+ OCR (GLM-OCR produces per-line LaTeX). Fixing this in the text-only path
171+ requires X-proximity-aware grouping — chars at different Y but overlapping
172+ X are part of the same expression. This is a known TODO.
173+
174+ ** Fragment break suppression** : tiny lines (≤3 chars) never cause heuristic
175+ breaks (y_break, x_break, font_break) because they're subscript/superscript
176+ fragments attached to adjacent content, not separate blocks.
177+
178+ ### Overlapping formula deduplication
179+ ` dedup_overlapping_formulas() ` ** merges** (not picks-one) overlapping formula
180+ regions. Requires both vertical AND horizontal overlap to prevent merging
181+ formulas across columns. This handles cases where loose_bounds inflate
182+ formula bboxes into overlapping territory.
183+
100184## Known Limitations
101185
102186### Text-only path
@@ -105,6 +189,7 @@ Then:
105189- ** Missing body content** : Some PDFs (computer_systems, programming_massively_parallel) have body pages that extract to empty.
106190- ** Font mapping failures** : lambda book — TeX fonts lack Unicode mappings.
107191- ** Margin notes** : Interleaved with body text in some books (EDO, fluids).
192+ - ** Fraction fragmentation** : Math fractions (` 1/Δt² ` ) produce separate lines for numerator, bar, and denominator in ` group_into_lines() ` . No Y-threshold alone can merge them without also merging real separate prose lines. Needs X-proximity-aware grouping.
108193
109194### Layout path
110195- Requires GPU for layout detection, formula OCR, and table OCR.
0 commit comments