extract_tables_in_regions: detect paragraph-as-table misreads (0.4.2)
Adds a 5th failure-mode check to looks_like_partial_table: when the heuristic mis-detects text-wrapped paragraph prose as a multi-column table, cells in the same column tend to start with lowercase letters or continuation punctuation (commas, closing quotes) — because they're actually sentence fragments. Real tables almost never have most data cells starting lowercase. Trigger: ≥2 cols, ≥4 data rows, ≥60% of non-empty data cells start with lowercase or continuation punctuation → return needs_ocr=true. Caught in the eval as the next-largest failure mode after the 0.4.1 fix: PDFs 088, 182, 090 — heuristic produced "tables" like: |Approval is needed from the|Acquisitions of| |Treasurer if the acquisition|residential and| |constitutes a "significant|agricultural| |action," including acquiring an|land by foreign| Reading column 1 top-to-bottom: "Approval is needed from the Treasurer if the acquisition constitutes a 'significant action,' including acquiring an interest..." — a paragraph, not tabular data. Tests: 2 new tests (the 088-style failure case + a real multi-word table that must NOT be flagged). All 11 looks_like_partial_table tests pass; 323 unit + 91 integration tests still green. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
d0dd067e70
commit
780efdb955
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "firecrawl-pdf-inspector",
|
"name": "firecrawl-pdf-inspector",
|
||||||
"version": "0.4.1",
|
"version": "0.4.2",
|
||||||
"description": "Fast PDF classification and text extraction. Detect text-based vs scanned PDFs, extract text by region with quality checks. Native Rust performance via napi-rs.",
|
"description": "Fast PDF classification and text extraction. Detect text-based vs scanned PDFs, extract text by region with quality checks. Native Rust performance via napi-rs.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"types": "index.d.ts",
|
"types": "index.d.ts",
|
||||||
|
|||||||
+78
@@ -1298,6 +1298,56 @@ fn looks_like_partial_table(markdown: &str) -> bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Failure mode 5: cells flow as continuation paragraph (text wrapping
|
||||||
|
// mistaken for column structure). When a paragraph of prose gets mis-
|
||||||
|
// detected as a multi-column table, cells in the same column tend to
|
||||||
|
// start with lowercase letters or punctuation (continuation), not
|
||||||
|
// capital letters / digits (new entries). Real tables almost never
|
||||||
|
// have most data cells starting lowercase.
|
||||||
|
//
|
||||||
|
// Signal: ≥2 cols, ≥4 data rows, and ≥60% of non-empty data cells
|
||||||
|
// start with a lowercase letter or continuation punctuation.
|
||||||
|
let data_rows: Vec<Vec<&str>> = lines
|
||||||
|
.iter()
|
||||||
|
.skip(2) // header + separator
|
||||||
|
.map(|l| {
|
||||||
|
let parts: Vec<&str> = l.split('|').map(|s| s.trim()).collect();
|
||||||
|
if parts.len() >= 3 {
|
||||||
|
parts[1..parts.len() - 1].to_vec()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(|cells| !cells.is_empty())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if n_cols >= 2 && data_rows.len() >= 4 {
|
||||||
|
let mut continuation = 0;
|
||||||
|
let mut total = 0;
|
||||||
|
for row in &data_rows {
|
||||||
|
for cell in row {
|
||||||
|
let trimmed = cell.trim();
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
total += 1;
|
||||||
|
let first = trimmed.chars().next().unwrap();
|
||||||
|
// Continuation indicators: lowercase letter, common
|
||||||
|
// mid-sentence punctuation, closing quote
|
||||||
|
if first.is_lowercase()
|
||||||
|
|| matches!(first, ',' | '.' | ';' | ')' | '"' | '\'' | '”' | '’')
|
||||||
|
{
|
||||||
|
continuation += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if total > 0 && continuation * 5 >= total * 3 {
|
||||||
|
// ≥60% of cells look like sentence continuations → paragraph
|
||||||
|
// misread as table.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1377,6 +1427,34 @@ mod looks_like_partial_table_tests {
|
|||||||
let md = "|A|B|C|D|\n|---|---|---|---|\n|x|y||z|\n|p|q|r|s|";
|
let md = "|A|B|C|D|\n|---|---|---|---|\n|x|y||z|\n|p|q|r|s|";
|
||||||
assert!(!looks_like_partial_table(md));
|
assert!(!looks_like_partial_table(md));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn paragraph_misread_as_two_column_table_is_partial() {
|
||||||
|
// Real production failure: text-wrapped paragraph mis-detected as
|
||||||
|
// 2-col table. Each cell continues the previous one as prose.
|
||||||
|
let md = "|Approval is needed from the|Acquisitions of|\n\
|
||||||
|
|---|---|\n\
|
||||||
|
|Treasurer if the acquisition|residential and|\n\
|
||||||
|
|constitutes a \"significant|agricultural|\n\
|
||||||
|
|action,\" including acquiring an|land by foreign|\n\
|
||||||
|
|interest in different types of|persons must be|\n\
|
||||||
|
|land where the monetary|reported to the|";
|
||||||
|
assert!(looks_like_partial_table(md));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn real_multi_word_table_is_kept() {
|
||||||
|
// Real table with multi-word entries — cells start with capital
|
||||||
|
// letters / proper nouns, NOT lowercase continuations.
|
||||||
|
let md = "|Country|Capital|Notes|\n\
|
||||||
|
|---|---|---|\n\
|
||||||
|
|United States|Washington DC|Federal capital|\n\
|
||||||
|
|United Kingdom|London|City of London is a separate|\n\
|
||||||
|
|France|Paris|Île-de-France region|\n\
|
||||||
|
|Germany|Berlin|Reunified 1990|\n\
|
||||||
|
|Spain|Madrid|Largest city in Spain|";
|
||||||
|
assert!(!looks_like_partial_table(md));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Analyse extracted items and rects for layout complexity.
|
/// Analyse extracted items and rects for layout complexity.
|
||||||
|
|||||||
Reference in New Issue
Block a user