Compare commits

...
Author SHA1 Message Date
Abimael Martell bfe3f55032 fix(tables): header bypass requires continuations to strictly outnumber rows
Align the code with its stated rule (the comparison allowed the bypass
at exact equality) and add the dedicated positive-path test: a compact
header atop parallel prose columns whose cross-row continuations
outnumber the rows is flagged as parallel prose. Bench unchanged.
2026-08-18 15:38:08 -07:00
Abimael Martell e0be3b5021 fix(tables): reject parallel-prose grids on all unsplit pages
The body-font heuristic pass projects multi-column text pages onto
table grids: on a two-column reference section, every line pair across
the gutter looks like a row with two X-clusters, and the page is
emitted as a woven table. The parallel-prose rejector — which requires
transition evidence (unterminated cells flowing into lowercase starts
in the same column), not mere cell length — was gated to chart pages;
it now runs for every unsplit page.

A compact header row still blocks the rejection, except when cross-row
prose continuations outnumber the rows: no genuine table produces a
continuation on average in every row, so the 'header' there is just
short line fragments atop parallel prose columns.

Band-split retries stay exempt: they exist for tables that only
assemble after recombining bands.
2026-08-18 15:23:21 -07:00
+45 -6
View File
@@ -635,7 +635,12 @@ fn is_parallel_prose_table(table: &crate::tables::Table) -> bool {
}
}
let is_parallel = !has_compact_header
// A compact header row is evidence for a real table — unless cross-row
// prose continuations outnumber the rows, which no genuine table
// produces: the "header" is then just two short line fragments at the
// top of parallel prose columns.
let header_blocks = has_compact_header && continuation_fragments <= table.cells.len();
let is_parallel = !header_blocks
&& non_empty >= 5
// Independent prose columns have asynchronous line/paragraph breaks;
// a fully populated grid is positive evidence for a real descriptive
@@ -1375,7 +1380,6 @@ pub(crate) fn to_markdown_from_items_with_rects_and_lines(
chart_page_prose_column_split(&page_layout_items)
.filter(|&split_x| chart_spans_prose_split(region, split_x))
});
let chart_prose_columns = chart_prose_split.is_some();
// Check for side-by-side table layout using the original items. Sparse
// numeric cells need table context before they can be distinguished
@@ -1616,10 +1620,16 @@ pub(crate) fn to_markdown_from_items_with_rects_and_lines(
if subset_items.len() < min_items {
return;
}
// Keep body-font detection available on chart pages: a real
// table can share the prose anchors. Reject only candidates
// whose cells prove they are parallel prose fragments.
let reject_parallel_prose = chart_prose_columns && !was_split;
// Reject candidates whose cells prove they are parallel
// prose fragments — the shape produced when the body-font
// pass projects a multi-column text page onto one table
// grid (two-column reference sections are the classic
// case). The check needs internal transition evidence
// (unterminated cells flowing into lowercase starts in
// the same column), so genuine tables with long cells
// pass. Band-split retries stay exempt: they exist for
// tables that only assemble after recombining bands.
let reject_parallel_prose = !was_split;
let tables = detect_tables_with_page_width(
subset_items,
base_size,
@@ -2674,6 +2684,35 @@ mod tests {
);
assert!(!is_parallel_prose_table(&data));
// A compact header row atop parallel prose columns: cross-row prose
// continuations outnumber the rows, so the header cannot save the
// candidate — this is page prose with two short fragments on top.
let headed_parallel_prose = crate::tables::Table::new(
vec![90.0, 340.0],
vec![340.0, 320.0, 300.0, 280.0, 260.0],
vec![
vec!["June 2023".into(), "Page 5".into()],
vec![
"the committee reviewed the proposal and decided that the".into(),
"funding for the second phase would continue subject to the".into(),
],
vec![
"implementation schedule should be extended by another".into(),
"quarterly reviews established during the first phase of the".into(),
],
vec![
"six months to accommodate the revised procurement rules".into(),
"".into(),
],
vec![
"adopted at the previous meeting of the governing board".into(),
"participating institutions across the partner regions".into(),
],
],
(0..10).collect(),
);
assert!(is_parallel_prose_table(&headed_parallel_prose));
let headed_text_table = crate::tables::Table::new(
vec![90.0, 340.0],
vec![320.0, 300.0, 280.0],