Your detector scope should match your content scope
TLDR
Tuning layout detection thresholds to handle multi-column pages without pre-partitioning content leads to continuous threshold oscillation. Running a table or column detector on text items that span multiple independent page columns creates statistical cross-contamination. Pre-splitting unclaimed text into isolated column zones before running spatial layout detectors ensures that every calculated metric operates on a single coherent content stream.| Processing Paradigm | Input Scope | Threshold Sensitivity | Failure Mode |
|---|---|---|---|
| In-Detector Multi-Column Tuning | Full Page (Interleaved columns) | High (Fragile magic thresholds) | Silent false positives / negatives |
| Pre-Split Zone Partitioning | Single Column Zone | Zero (Stable, honest thresholds) | Graceful degradation |
Problem statement: the fallacy of in-detector multi-column modes
When document processing detectors fail on multi-column layouts, developers often add "multi-column modes" with tightened thresholds (e.g., lower fill rate requirements, stricter item count caps).
This approach relies on coincidence: pathological cross-column inputs happen to fail tighter thresholds on specific test documents.
However, inter-column gaps vary based on layout geometry, font sizes, and margin styles. No single global threshold set accurately distinguishes left-column tables from right-column prose across arbitrary layouts.
Technical architecture: aligning detector scope with content scope
Instead of tuning thresholds to handle mixed inputs, partition the input domain into isolated single-column content zones before running spatial layout detectors:
[Unclaimed Text Items] ---> [Page Column Splitter] ---> [Zone 1 Items] ---> [Detector (Zone 1)]
---> [Zone 2 Items] ---> [Detector (Zone 2)]
Pre-Split zone orchestration
export function runScopedLayoutDetection(unclaimedItems, viewport, scale) {
// 1. Determine vertical page column splits across unclaimed text
const { splits } = detectPageColumns(unclaimedItems, viewport, scale);
const splitXs = splits.map(s => s.x).filter(x => x > viewport.width 0.1 && x < viewport.width 0.9);
// 2. Fallback to full-page detection if no column splits exist if (splitXs.length === 0) { return detectStreamTables(unclaimedItems, scale); }
// 3. Partition text items into isolated column zones const boundaries = [-Infinity, ...splitXs, Infinity]; const detectedRegions = [];
for (let i = 0; i < boundaries.length - 1; i++) { const minX = boundaries[i]; const maxX = boundaries[i + 1];
const zoneItems = unclaimedItems.filter(item => item.vx >= minX && item.vx < maxX); if (zoneItems.length < 6) continue;
// Detect strictly within single-column scope const zoneResults = detectStreamTables(zoneItems, scale, { isZoneMode: true }); detectedRegions.push(...zoneResults); }
return detectedRegions; }
Rule of thumb: Match detector input scope to content domain scope. Partition multi-column page text into isolated zones before running spatial layout algorithms.