fix: improve table detection for numeric columns and multi-line headers (#35)

* fix: improve heuristic table detection for numeric columns and multi-line headers

Two fixes for tables that have clean extractable text but fail heuristic
structure detection:

1. Numeric column merge pass (grid.rs): After initial X-position
   clustering, adjacent clusters are merged when one is sparse (header
   text) and the other is dense with >50% numeric items (data column).
   Multi-line wrapped headers often land slightly offset from their
   data column — the merge closes gaps within 1.5× the clustering
   threshold. New is_numeric_text() helper matches decimals, percentages,
   negative numbers, and comma-separated thousands.

2. Duplicate-header skip (detect_heuristic.rs): Spanning super-headers
   like "First Degree | First Degree | Higher Degree" contain duplicate
   cells that trigger looks_like_partial_table_ex rejection. Now skips
   rows with duplicate cells when a better header candidate exists
   within the next 3 rows (higher fill ratio or numeric cells).

Tested on BITS Pilani university report (430 pages, 314 table pages).
Page 4 (multi-line header + numeric data) previously returned
needs_ocr=true; now correctly detects the table structure.

Eval: 197 PDFs, zero regressions, all 104+ tests pass, zero clippy.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* bump version to 0.7.1

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Abimael Martell
2026-04-14 17:27:59 -07:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 35445c3208
commit 7c8b09be67
5 changed files with 196 additions and 13 deletions
Binary file not shown.
+36
View File
@@ -1438,6 +1438,42 @@ fn test_extract_tables_in_regions_nonexistent_page() {
assert!(region.text.is_empty());
}
#[test]
fn test_bits_pilani_page4_table_detection() {
// Page 4 (0-indexed 3) has a table with multi-line wrapped headers and
// numeric data columns. The heuristic detector previously failed because:
// 1. Header items at different X positions than data created extra column
// clusters (6 cols instead of 4)
// 2. Spanning super-header row ("First Degree | First Degree") produced
// duplicate header cells that looks_like_partial_table_ex rejected
let buf = std::fs::read("tests/fixtures/bits_pilani_feedback.pdf").unwrap();
let results =
extract_tables_in_regions_mem(&buf, &[(3, vec![[0.0, 0.0, 612.0, 792.0]])]).unwrap();
assert_eq!(results.len(), 1);
let region = &results[0].regions[0];
assert!(
!region.needs_ocr,
"Page 4 table should be detected, got needs_ocr=true"
);
assert!(
region.text.contains("BIO"),
"Should contain department name BIO"
);
assert!(region.text.contains("8.23"), "Should contain numeric data");
}
#[test]
fn test_bits_pilani_page8_table_detection() {
// Page 8 (0-indexed 7) has a numbered-row table that already worked.
// Verify it still works after changes.
let buf = std::fs::read("tests/fixtures/bits_pilani_feedback.pdf").unwrap();
let results =
extract_tables_in_regions_mem(&buf, &[(7, vec![[0.0, 0.0, 612.0, 792.0]])]).unwrap();
assert_eq!(results.len(), 1);
let region = &results[0].regions[0];
assert!(!region.needs_ocr, "Page 8 table should still be detected");
}
// =========================================================================
// extract_pages_markdown_mem tests
// =========================================================================