Implement confirm_columns function that partitions page into candidate
columns (regions between consecutive gaps + before-first + after-last),
counts unique lines whose first span's x0 falls within each candidate's
x-range, and promotes candidates with line_count >= 3 to confirmed columns.
Supporting code:
- ColumnGap struct with lo/hi bounds, width(), midpoint()
- detect_column_gaps function for zero-coverage region detection
- HasFirstSpan trait for first span bbox access
- CandidateColumn struct for tracking x_range and line_count
All 49 column tests pass, including all acceptance criteria.
Bead: pdftract-2rkc1
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Phase 4.5 XY-cut reading order determination for block-level layout analysis.
Implementation:
- xy_cut() function with recursive widest-whitespace split
- Vertical split first (columns dominate), then horizontal split
- Single column detection via gap analysis (blocks on both sides of gap)
- Projection histogram for robust gap detection (1-point bins)
- MAX_DEPTH=20 to prevent stack overflow
- XYCutResult with order, region_count, small_region_count, algorithm
Acceptance criteria (PASS):
- 2-column page: all left-column blocks before all right-column blocks
- 3-column page: col0, col1, col2 order preserved
- Single column: top-to-bottom order (y descending)
- Full-width heading + 2 columns: heading first, then columns
- Small region count signals Docstrum trigger (>10 regions with <3 blocks)
- All unit tests pass
Module: crates/pdftract-core/src/layout/reading_order.rs
Tests: 16 tests covering basic cases, edge cases, split detection
Closes: pdftract-4md5z
Fixes:
- Corrected test_color_device_rgb_clamped expected value from "#ff8080" to "#ff0080"
(G value -0.5 should clamp to 0.0, not 0.5)
- Fixed lifetime annotation in readability.rs (Cow<str> -> Cow<'_, str>)
- Fixed unused_must_use warning in page_class.rs test
Verification (notes/pdftract-tuky.md):
- All 8 children of Phase 3.1 coordinator are closed
- q/Q 64-level depth limit verified (test_64_nested_q_calls_succeed)
- Td chain accumulation verified (test_td_chain)
- Tm/Td ordering correct per ISO 72-bit spec
- /Rotate normalization implemented in child pdftract-1jlpy
- All 6 color operators tracked (72 graphics_state tests pass)
Closes: pdftract-tuky
- Fixed missing fields in BlockJson, SpanJson, ExtractionOptions initializations
- Added feature gates to ocr_integration tests for conditional compilation
- Fixed McpServerState::new calls to include audit writer argument
- Fixed CCITTFaxDecoder::decode calls to use instance method
- Fixed type casts for ObjRef::new calls
- Fixed serde_json::Value method calls (is_some -> !is_null)
- Fixed ProfileType test feature gates
- Worked around lifetime issues in schema roundtrip tests
These changes fix numerous compilation errors that were blocking the
codebase from building. The main library and tests now compile successfully.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Implement repair_hyphenation() that detects and repairs end-of-line
hyphenation within blocks. Joins hyphenated words across line breaks
when the hyphen is at the column right edge and the continuation
starts with a lowercase letter.
Key features:
- Detects hyphens: -, ‐ (U+2010), ‑ (U+2011), soft hyphen (U+00AD)
- Right-edge detection: span bbox.x1 within 5% of column width
- Lowercase continuation check to avoid joining sentences
- Column-aware: only joins spans in same column
- Cleans up empty spans/lines after repair
Adds HasBBox and HyphenableSpan traits for flexible span types.
Includes 9 comprehensive tests covering all acceptance criteria.
Fixes pre-existing test cases in schema module (missing column field).
Closes: pdftract-5o6hx
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Implement cluster_spans_into_lines for Phase 4.2 line formation.
Groups spans into lines by baseline proximity using threshold 0.5 * median_font_size.
- Add HasFontSize trait for types with font_size
- Implement cluster_spans_into_lines function
- Compute baseline for each span
- Sort by baseline ASC
- Sweep and cluster within threshold
- Emit Line per cluster
- Sort spans by x0 within each line
- Add finalize_line_cluster helper
- Export new items from layout module
Tests: All 11 acceptance criteria tests pass
- Spans baselines 100, 100.5, 105 with median 12: one line
- Spans baselines 100, 110 with median 12: two lines
- Superscript stays on same line as base text
- Empty input produces empty output
- Threshold is 0.5 * median_font_size (not hardcoded)
Closes: pdftract-6bwq4
Implement Phase 4.4 code block classification for detecting indented
monospace code blocks.
Features:
- is_monospace_font_name: Check font name for monospace indicators
(mono, courier, code, fixed, console - case-insensitive)
- is_fixed_pitch_flag: Check FontDescriptor bit 0 (FixedPitch)
- classify_code: Classify block as code if all spans monospace AND
indented ≥ 2em from column baseline
- classify_page_code_blocks: Post-processing pass to upgrade paragraph
blocks to code kind
Acceptance criteria:
- All-Courier, indented 24pt, font_size 12pt (2em=24): Code ✓
- All-monospace, not indented: NOT Code ✓
- Mixed serif+monospace: NOT Code ✓
- One serif span at end: NOT Code ✓
- FixedPitch flag set, no "Mono" in name: STILL Code ✓
Closes: pdftract-8n270
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Implement compile-time phf::Set of 20,000 common English words for
dictionary coverage scoring in readability analysis (Phase 4.7).
Key changes:
- Added wordlist-en-20k.txt (20k frequency-sorted English words)
- Extended build.rs to generate phf::Set from wordlist
- Added layout/wordlist.rs module with is_english_word() API
- Added wordlist benchmarks (< 100 ns lookup achieved)
Test results:
- All 9 unit tests pass
- Benchmarks: 13-62 ns per lookup (well under 100 ns requirement)
- Binary size: Estimated ~200-220 KB (within 250 KB limit)
Closes: pdftract-9wevc
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Implement per-word validation filter for assisted-OCR BrokenVector path.
Changes:
- Add SpanSource::OcrAssisted variant to hybrid.rs
- Add Span::ocr_assisted() helper method
- Implement validate_ocr_with_position_hints() in ocr.rs
- 5pt distance threshold for position validation
- 0.4 confidence cap for rejected words
- Linear scan for nearest-neighbor lookup
- Add unit tests for validation filter
Closes: pdftract-3s2i
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Implement char-weighted median aggregation of per-span readability
scores into a page-level score stored in extraction_quality.readability.
Algorithm:
- Collect (score, char_count) pairs from spans
- Sort by score ascending
- Walk sorted list accumulating character counts
- Return score at half-total-char position
Acceptance criteria:
- Single span: returns its score
- Multiple spans: char-weighted median (longer spans count more)
- Empty page: returns 0.0
- All-perfect: returns 1.0
Closes: pdftract-oh30a
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add Phase 4 caption classification for detecting figure captions.
Implements classify_caption() which identifies blocks as captions when:
- Small font size (median < page body median)
- Follows Figure block within 2 line heights
- Same column as Figure
Module: crates/pdftract-core/src/layout/caption.rs
Acceptance criteria:
- Block immediately below Figure, small font, same column → kind: Caption
- Block 5 lines below Figure → NOT Caption (gap too large)
- Block with body-size font below Figure → NOT Caption (font not smaller)
- Block in different column from Figure → NOT Caption
Tests: 9/9 passed covering all acceptance criteria plus edge cases.
Closes: pdftract-xzfkt
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>