Pdf Processor
Four Bugs in One Algorithm Design
TLDR
During the design review of our v1 bipartite column detection algorithm, four compounding mathematical edge-case bugs were identified prior to shipping to production: reversed gutter tolerance directions, viewport-frame instead of content-frame persistence checks, global band count denominator pollution, and two-pointer interval scan failures. Correcting these four geometric constraints produced a bulletproof multi-column layout engine.| Geometric Defect | Faulty Math Formulation | Structural Consequence | Corrected Formulation |
|---|---|---|---|
| Reversed Tolerance | maxX <= X + tol | Overlapping bands counted as clean split | maxX <= X - tol (Shrinks inward) |
| Viewport Persistence | b.y <= vpHeight 0.20 | Rejects short 2-column articles | b.y <= contentTop + contentSpan 0.20 |
| Global Denominator | / narrowBands.length | Rejects 2-column sections on hybrid pages | / localCoexistenceBands.length |
| Two-Pointer Walk | Un-merged maxX/minX lists | Misses overlapping band spans | Interval merging prior to gap search |
Technical defect analysis & fixes
Defect 1: reversed gutter tolerance direction
- Faulty Logic: Adding tolerance outwards (
maxX <= X + tol) allowed bands ending past the split center to count as "committed left," causing overlapping bands to evaluate as a clean partition. - Correction: Subtract tolerance inward (
maxX <= X - tol) to ensure committed populations never overlap spatially.
// REFACTORED: Inward Tolerance Partitioning
const leftOnly = narrowBands.filter(b => b.maxX <= X - tol);
const rightOnly = narrowBands.filter(b => b.minX >= X + tol);
Defect 2: viewport-relative persistence scaling
Faulty Logic: Evaluating header confinement againstviewportHeight 0.20 rejected valid 2-column layouts on pages where text occupied only the top half of the page.
- Correction: Measure persistence relative to the actual vertical content span (
contentBottom - contentTop).
// REFACTORED: Content-Relative Persistence
const contentTop = Math.min(...narrowBands.map(b => b.y));
const contentBottom = Math.max(...narrowBands.map(b => b.y));
const contentSpan = contentBottom - contentTop || 1;
const persistenceThreshold = contentTop + contentSpan * 0.20;
Defect 3: global denominator pollution on hybrid layouts
- Faulty Logic: Dividing committed bands by total page bands (
narrowBands.length) rejected 2-column sections located on pages that were 70% single-column. - Correction: Restrict the commitment ratio denominator to the Y-range intersection of left and right populations (the Coexistence Zone).
// REFACTORED: Coexistence Zone Denominator
const coexistTop = Math.max(Math.min(...leftOnly.map(b => b.y)), Math.min(...rightOnly.map(b => b.y)));
const coexistBottom = Math.min(Math.max(...leftOnly.map(b => b.y)), Math.max(...rightOnly.map(b => b.y)));
if (coexistBottom < coexistTop) return false; // Populations are vertically disjoint
const localCoexistenceBands = narrowBands.filter(b => b.y >= coexistTop && b.y <= coexistBottom); const commitmentRatio = (leftOnly.length + rightOnly.length) / localCoexistenceBands.length;
if (commitmentRatio < 0.40) return false;
Defect 4: two-pointer interval overlap failures
- Faulty Logic: Sorting
maxXandminXinto independent arrays failed to track interval overlap state. - Correction: Execute interval merging prior to candidate gap evaluation.
// REFACTORED: Interval Merging Before Candidate Search
const sortedBands = [...narrowBands].sort((a, b) => a.minX - b.minX);
const mergedSpans = [];
for (const band of sortedBands) { if (mergedSpans.length && band.minX <= mergedSpans.at(-1).hi) { mergedSpans.at(-1).hi = Math.max(mergedSpans.at(-1).hi, band.maxX); } else { mergedSpans.push({ lo: band.minX, hi: band.maxX }); } }
Rule of thumb: Evaluate persistence relative to actual content bounds rather than page frames, and calculate layout ratios within local coexistence intersections.
Read this post in the full Engineering Journal →