PDF tables look simple until you actually extract one. The visual structure in your reader is a rendering artifact. The underlying file has no table object, no row, no cell: just positioned text spans and optional line segments. When a table has merged cells, two or more content spans share the same logical cell, but the PDF records each span at an independent coordinate. The parser sees overlapping positions and has to guess where one cell ends and another begins.
Standard extraction libraries handle this badly, and they handle it badly in different ways.
What the standard tools do
pdfplumber uses horizontal and vertical rules to infer table boundaries, then assigns each text span to the nearest box. When a header cell spans two columns, its text lands in whichever column center is closest. The sibling column gets an empty string. The row count comes out right, but the column attribution is wrong for the merged cell and everything below it in that column.
camelot lattice mode traces the actual line segments to find cells, which makes it better on bordered tables. But merged cells produce intersections at unexpected positions, and the line-tracing algorithm misinterprets them as separate narrow cells. You end up with phantom columns. The stream mode, which ignores borders and uses whitespace, fares worse on multi-column layouts where columns sit close together.
tabula-py (the JVM wrapper around Tabula) splits into lattice and stream modes with the same failure patterns.
Where multi-column layouts make it worse
The problem compounds in multi-column page layouts. Regulatory filings from Korean DART, Japanese EDINET, and Hong Kong HKEX often embed tables inside a two-column page layout. Two tables appear side by side as page columns. A column in the left table overlaps the horizontal coordinate range of a cell in the right table. Position-based heuristics cannot tell which table a text span belongs to without first knowing where each table's boundary is, which is exactly what the parser is trying to find.
I ran into this on balance sheet tables from DART filings: Korean text, two columns on the page, the first containing account names and the second containing values for two reporting periods in adjacent sub-columns. A merged header cell labeled "Current Period" and another labeled "Prior Period" each spanned two numeric sub-columns. pdfplumber placed all four value columns correctly but dropped the period labels. camelot lattice created six columns where there were four, adding ghost columns at the merge intersection points.
What actually worked
The approach that reduced the error rate was two-stage.
First, check whether the table has explicit line segments. If it does, trace the lattice manually and mark every merge point explicitly. A cell spanning columns 2 and 3 gets tagged as a merge, not split into phantom cells. If the table is borderless, fall back to whitespace-based column detection, but seed the column boundaries from the page-level column separator, not from the table's own whitespace. That separator appears as a consistent horizontal gap across the full page height, visible even when individual table rows have variable internal spacing.
For merged cell attribution, the heuristic I settled on is propagation: a header cell spanning columns N through M assigns its text to column N and marks columns N+1 through M as inherited from N. Values in those sub-columns are tagged with the inherited header during downstream processing.
This is not a general solution. Hierarchical merges, where a cell spans both rows and columns, still misfire on documents where the rendering is too compact. The challenge is that row-spanning merges appear as text positioned between two horizontal lines with no structural signal that the text covers more than one row. A parser that does not store the merge map as a first-class object loses that information before it can propagate it.
The underlying limit
The PDF format is the root constraint. Tables are rendered, not encoded. The format specification has no native concept of a merged cell. What you see as a single spanning cell is typically one text element positioned in the visual center of the merged area, with line segments drawn around it. A compliant PDF renderer knows to draw that correctly. An extractor working from coordinate data has to reconstruct the intent from geometry, and geometry is ambiguous at merge points.
The only reliable path I found is to not treat extraction as a pure parsing problem. The geometry pass identifies candidates. A small verification step checks whether the extracted numeric columns sum to stated totals in the document. Mismatches flag cells that were likely misfiled during extraction. That check is separate from the extraction itself, but it is the part that surfaces the errors the geometry heuristics miss.
The full merge detection approach, including how the verification pass catches misfiled cells, is at https://hannune.ai/blog/merged-cells-pdf-table-extraction
Top comments (0)