Engineering Journal
Pdf Processor
Pdf Processor

Detecting a column split is not the same as locating it

2026-07-10

TLDR

Layout detectors frequently identify column split existence accurately while placing split coordinates off by $15\text{ to }32\text{px}$ due to ragged-right text clustering. Loosening spatial containment tolerances to absorb coordinate errors causes centered titles and email headers to be absorbed into columns. A bounded valley scan post-pass snaps detected split coordinates to true whitespace channels using coverage histograms without altering split counts.
Detector StepTechnical ResponsibilityCoordinate AccuracyOutput Result
Pass 1: Split DetectionConfirms 2-column existenceOff by $32\text{px}$ ($X=566\text{px}$)Body lines misclassified as full-width
Pass 2: Valley Scan RelocationSnaps $X$ to local whitespace valleyExact ($X=598\text{px}$)Clean 2-column body reflow

Technical problem analysis

In a 16-page two-column document, column detection reported a split at $X = 566\text{px}$. The actual physical gutter center was $X = 598\text{px}$.

Because $566\text{px}$ fell inside the left column text region, 13 body lines spanning $144\text{px}$ to $581\text{px}$ straddled the boundary by $15\text{px}$, causing them to fall out of their column and render as full-width blocks across the page center.


Technical solution: bounded valley scan post-pass

Perform a localized histogram scan bounded to 3 body-font heights around the initial split coordinate:

export function snapSplitToWhitespaceValley(detectedSplitX, textItems, bodyFontHeightPx = 12) {
  const scanWindowPx = bodyFontHeightPx * 3; // Bounded search window radius
  const lo = detectedSplitX - scanWindowPx;
  const hi = detectedSplitX + scanWindowPx;

// Compute coverage histogram stepping 2px across search window const coverageHistogram = computeCoverageHistogram(textItems, lo, hi, 2); const minCoverage = Math.min(...coverageHistogram.counts);

// Valley threshold tolerates full-page footer lines crossing the center const valleyThreshold = minCoverage + Math.max(2, Math.ceil(minCoverage * 0.5)); const valleyRun = findWidestRunBelowThreshold(coverageHistogram, valleyThreshold);

// Safety Guards: Ignore runs touching window edges or narrower than 0.75x font size if (valleyRun.widthPx < bodyFontHeightPx * 0.75 || valleyRun.touchesWindowEdge) { return detectedSplitX; // Fail-safe: keep original coordinate }

return valleyRun.centerX; }

Verification results

Rule of thumb: Separate column existence detection from coordinate localization, using bounded valley scans to snap split coordinates to physical whitespace channels.
Read this post in the full Engineering Journal →