Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46e23fb910 | ||
|
|
7afe4ac04d | ||
|
|
e7487be94b |
@@ -1477,6 +1477,10 @@ fn detect_row_stripe_table(
|
|||||||
debug!(" row-stripe rejected: sparse outline/prose continuation shape");
|
debug!(" row-stripe rejected: sparse outline/prose continuation shape");
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
if has_dominant_prose_cell(&cells) {
|
||||||
|
debug!(" row-stripe rejected: dominant prose cell (chart/figure region over body text)");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
let column_centers: Vec<f32> = (0..num_cols)
|
let column_centers: Vec<f32> = (0..num_cols)
|
||||||
.map(|c| (col_edges[c] + col_edges[c + 1]) / 2.0)
|
.map(|c| (col_edges[c] + col_edges[c + 1]) / 2.0)
|
||||||
@@ -1495,6 +1499,34 @@ fn detect_row_stripe_table(
|
|||||||
Some(Table::new(column_centers, row_centers, cells, item_indices))
|
Some(Table::new(column_centers, row_centers, cells, item_indices))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Detect a grid that swallowed body text instead of tabular data.
|
||||||
|
///
|
||||||
|
/// Charts (bar graphs, axis gridlines) emit fields of drawing rects that can
|
||||||
|
/// pass the row-stripe shape test; the resulting "table" then captures the
|
||||||
|
/// page's prose. The signature: one cell holds an entire paragraph — ≥60 words
|
||||||
|
/// AND at least a third of all words in the table.
|
||||||
|
///
|
||||||
|
/// There is deliberately no row-count exemption. A small table whose single
|
||||||
|
/// long cell dominates its word count is indistinguishable by content from a
|
||||||
|
/// phantom grid over body text, and across the regression corpora every such
|
||||||
|
/// grid observed has been swallowed prose, never a real note table. The costs
|
||||||
|
/// are also asymmetric: rejecting a real table degrades it to readable prose,
|
||||||
|
/// while accepting a phantom scrambles the page into Y-interleaved cells.
|
||||||
|
/// Larger legitimate tables are safe because the one-third-of-total threshold
|
||||||
|
/// scales with table size.
|
||||||
|
fn has_dominant_prose_cell(cells: &[Vec<String>]) -> bool {
|
||||||
|
let mut total_words = 0usize;
|
||||||
|
let mut max_cell_words = 0usize;
|
||||||
|
for row in cells {
|
||||||
|
for cell in row {
|
||||||
|
let words = cell.split_whitespace().count();
|
||||||
|
total_words += words;
|
||||||
|
max_cell_words = max_cell_words.max(words);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
max_cell_words >= 60 && max_cell_words * 3 >= total_words
|
||||||
|
}
|
||||||
|
|
||||||
fn row_stripe_is_sparse_prose_outline(cells: &[Vec<String>]) -> bool {
|
fn row_stripe_is_sparse_prose_outline(cells: &[Vec<String>]) -> bool {
|
||||||
let Some(num_cols) = cells.first().map(|row| row.len()) else {
|
let Some(num_cols) = cells.first().map(|row| row.len()) else {
|
||||||
return false;
|
return false;
|
||||||
@@ -2294,6 +2326,12 @@ fn detect_merged_cluster_table(
|
|||||||
);
|
);
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
if has_dominant_prose_cell(&cells) {
|
||||||
|
debug!(
|
||||||
|
" merged-cluster rejected: dominant prose cell (chart/figure region over body text)"
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
// No empty columns
|
// No empty columns
|
||||||
for col in 0..num_cols {
|
for col in 0..num_cols {
|
||||||
@@ -2398,6 +2436,89 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- has_dominant_prose_cell ---
|
||||||
|
|
||||||
|
fn cells_of(rows: &[&[&str]]) -> Vec<Vec<String>> {
|
||||||
|
rows.iter()
|
||||||
|
.map(|r| r.iter().map(|c| c.to_string()).collect())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dominant_prose_cell_rejects_swallowed_paragraph() {
|
||||||
|
// Two cells hold paragraphs (the shape every observed phantom grid
|
||||||
|
// has: swallowed body text spans multiple cells), rest are chart labels
|
||||||
|
let para = ["word"; 70].join(" ");
|
||||||
|
let para2 = ["word"; 35].join(" ");
|
||||||
|
let cells = cells_of(&[
|
||||||
|
&[para.as_str(), "81", "76"],
|
||||||
|
&[para2.as_str(), "56", "9"],
|
||||||
|
&["2019", "2020", ""],
|
||||||
|
]);
|
||||||
|
assert!(has_dominant_prose_cell(&cells));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dominant_prose_cell_rejects_small_table_dominated_by_one_cell() {
|
||||||
|
// Boundary case, documented as INTENDED: a small grid whose single
|
||||||
|
// long cell dominates the word count is rejected even at 4+ rows.
|
||||||
|
// By content alone this shape is indistinguishable from a phantom
|
||||||
|
// grid over body text, and every observed instance in the regression
|
||||||
|
// corpora was swallowed prose (chart/figure regions), not a real
|
||||||
|
// note table. Rejection degrades gracefully — the text is still
|
||||||
|
// extracted as prose — while accepting a phantom scrambles reading
|
||||||
|
// order.
|
||||||
|
let note = ["word"; 70].join(" ");
|
||||||
|
let cells = cells_of(&[
|
||||||
|
&["Purpose", note.as_str()],
|
||||||
|
&["Owner", "Facilities team"],
|
||||||
|
&["Date", "2024-06-01"],
|
||||||
|
&["Status", "Active"],
|
||||||
|
]);
|
||||||
|
assert!(has_dominant_prose_cell(&cells));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dominant_prose_cell_allows_description_column() {
|
||||||
|
// Long-ish description cells, but text is spread across the table
|
||||||
|
let desc = ["word"; 25].join(" ");
|
||||||
|
let cells = cells_of(&[
|
||||||
|
&["Item A", desc.as_str(), "100"],
|
||||||
|
&["Item B", desc.as_str(), "200"],
|
||||||
|
&["Item C", desc.as_str(), "300"],
|
||||||
|
&["Item D", desc.as_str(), "400"],
|
||||||
|
]);
|
||||||
|
assert!(!has_dominant_prose_cell(&cells));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dominant_prose_cell_allows_short_tables() {
|
||||||
|
let cells = cells_of(&[&["Name", "Value"], &["Total", "42"]]);
|
||||||
|
assert!(!has_dominant_prose_cell(&cells));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dominant_prose_cell_allows_data_table_with_long_note() {
|
||||||
|
// A real 4+ row table with one verbose remark cell: the note is ≥60
|
||||||
|
// words but the table's other content carries more than 2× its word
|
||||||
|
// count, so concentration stays below the 1/3 threshold. The
|
||||||
|
// denominator scales with table size — this is what keeps large
|
||||||
|
// legitimate tables safe where a bare length cap would not.
|
||||||
|
let note = ["word"; 60].join(" ");
|
||||||
|
let row_text = ["data"; 12].join(" ");
|
||||||
|
let mut rows: Vec<Vec<String>> = (0..11)
|
||||||
|
.map(|i| {
|
||||||
|
vec![
|
||||||
|
format!("Item {i}"),
|
||||||
|
row_text.clone(),
|
||||||
|
format!("{}", i * 100),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
rows.push(vec!["Note".into(), note, String::new()]);
|
||||||
|
assert!(!has_dominant_prose_cell(&rows));
|
||||||
|
}
|
||||||
|
|
||||||
// --- rects_overlap ---
|
// --- rects_overlap ---
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user