docling-project / docling-project/docling

Table cell matching merges two columns when a PDF textline spans them; the resulting cell overlaps the neighbouring cell in the same row

Open
#4,189 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
Python
Stars
66.4k
Forks
4.8k
Avg merge
2d 21h
Merged PRs (30d)
84

Description

## Summary

When a PDF stores a row's leading field and the next field on one baseline with a
gap no wider than a word space, `docling-parse` returns them as a single
**textline** cell, and TableFormer's cell matching assigns that whole line to the
first column. The structure prediction is correct; the cell *content* is not.

The result is self-inconsistent in a way the library can detect: **the cell
assigned to column 0 overlaps, horizontally, the cell assigned to column 1 in the
same row.**

## Observed

A four-column ruled table, `Datum | Erläuterung | Betrag Soll EUR | Betrag Haben
EUR`. Predicted shape `15x4` — correct.

Header row, showing where the columns actually are:

| cell | column | x-range |
|---|---|---|
| `'Datum'` | 0 | 70.9 – 100.8 |
| `'Erläuterung'` | 1 | 123.0 – 176.1 |
| `'Betrag Soll EUR'` | 2 | 394.2 – 464.0 |
| `'Betrag Haben EUR'` | 3 | 486.0 – 568.3 |

First data row:

| cell | column | x-range |
|---|---|---|
| `'07.01.2025 Basislastschrift CREDITOR NAME REDACTED S.C.A.'` | **0** | **69.9 – 265.2** |
| `'304-0000000-0000000 XXXXXXXXXXXXXX'` | 1 | 123.3 – 218.1 |

The column-0 cell runs to x=265.2, well past the column-1 header at x=123.0, and
overlaps the column-1 cell of its own row across almost its entire width. Only
`'07.01.2025'` belongs in column 0.

## Where it originates

Not in the structure prediction, and not in the backend. `docling-parse` reports
the date and the first description word as one **textline** cell:

```
textline cell : '07.01.2025 Basislastschrift' bbox x 69.9 .. 191.5
```

At **word** level they are separate — no word cell spans the two columns. The
merge is at line level, and it is metrically reasonable in isolation: the gap
between the date and the following word is about the width of a word space, so
no threshold on horizontal distance alone separates "word gap" from "column
gap" here.

All three PDF backends produce the identical fused cell, which is consistent
with the cause being the line merge rather than any backend's geometry:

| backend | tables | words in table cells | cells matching `^date\s+\S` |
|---|---|---|---|
| `threaded_docling_parse` (default) | 13 | 3 991 | 134 |
| `dlparse_v4` | 14 | 4 120 | 131 |
| `pypdfium2` | 14 | 4 452 | 133 |

## The documented workaround makes it worse

`do_cell_matching`'s own comment anticipates this case:

```python
do_cell_matching: bool = (
True
# True: Matches predictions back to PDF cells. Can break table output if PDF cells
# are merged across table columns.
# False: Let table structure model define the text cells, ignore PDF cells.
)
```

Setting it to `False` on this document:

| | cells matching `^date\s+\S` | words in table cells |
|---|---|---|
| `do_cell_matching=True` | 134 | 3 991 |
| `do_cell_matching=False` | **159** | **3 349 (−17.8%)** |

More fused cells, and a sixth of the text gone. On a second document (137 tables,
a technical export) it did reduce fusion 211 → 45, but lost 7% of the words and
emitted duplicated header cells. It is not a usable workaround in either
direction.

## Reproducing without our file

The document is a personal bank statement and cannot be shared. The case should
reconstruct from:

- a ruled table, four columns, header row `Datum | Erläuterung | …`
- column 0 narrow (~30 pt of text), column 1 starting ~22 pt after column 0's
text ends
- each data row: a `dd.mm.yyyy` date in column 0 and text in column 1 **on the
same baseline**, with the horizontal gap between the date and the first word of
column 1 close to the font's space width
- column 1's text wrapping onto further lines below, which are *not* fused —
only the first line of each row is affected

`do_ocr=False`, `TableFormerMode.ACCURATE`.

## Suggested fixes

1. **Split a matched PDF cell at a predicted column boundary** when the boundary
falls inside the cell's bounding box. The prediction is already correct here,
and the inconsistency is detectable without it: a cell assigned to column *n*
should not horizontally overlap a cell assigned to column *n+1* in the same
row.
2. **Prefer word cells over textline cells when matching inside a table region.**
The words are correctly separated already; only the line merge loses the
boundary.
3. **Expose the merge thresholds.** `docling_parse`'s `DecodeConfig` has
`horizontal_cell_tolerance` and `word_space_width_factor_for_merge`, but
`docling` builds the `DecodeConfig` internally and plumbs only
`enforce_same_font`
(`docling/backend/docling_parse_backend.py::_make_docling_parse_decode_config`),
so a caller cannot reach them from `PdfPipelineOptions` — nor from
`docling-serve`, whose convert options expose only `pdf_backend` and
`table_cell_matching`.

(1) or (2) would fix it; (3) would let callers whose corpus has this shape work
around it in the meantime.

## Environment

`docling-serve` 1.31.0, `docling` 2.124.0, `docling-core` 2.93.0,
`docling-ibm-models` 4.0.1, `docling-parse` 7.16.0, image
`ghcr.io/docling-project/docling-serve-cu130:main`, Python 3.12, CUDA 13.

## Scale

Measured across an archive of 6 599 PDFs (14 330 tables in 3 849 documents). A
narrow leading column of dates or reference numbers beside a wide description is
the most common table shape in it, so this affects a large share of the tables we
extract. In the 13-table statement above, up to 134 of 479 cells match the fused
pattern — an upper bound, since the pattern also matches cells that legitimately
begin with a date, such as a `"Kontostand am 03.01.2025, Auszug Nr. 1"` heading.

(Payment and mandate references above are redacted, with character counts
preserved so the quoted x-ranges remain consistent.)

Contributor guide

Open the contributing guide

Research direction

Start in docling/backend/docling_parse_backend.py at _make_docling_parse_decode_config and trace do_cell_matching through table cell matching; compare the textline and word-cell paths described in the report. A fix should prevent a matched cell from overlapping the next column while preserving the separated words, and should be checked against the ruled-table reproduction described here.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
backend, data
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.