Table Cell Text Matching#
Overview#
After TableFormer predicts a table grid, Docling must assign raw text extracted from the PDF page into each predicted cell. This assignment is done via bounding-box overlap thresholds — a purely geometric mechanism with no typographic awareness, no user-configurable parameters, and no observability when text fails to match.
There are two distinct code paths depending on which model version is active:
- TableFormer V1 (
table_structure_model.py) — delegates toTFPredictor.multi_table_predict(do_matching=True)fromdocling-ibm-models. Whendo_matching=False, the fallback ispage._backend.get_text_in_rect(bbox). - TableFormer V2 (
table_structure_model_v2.py) — implements matching inline via_match_text().
The Overlap Thresholds#
V2: _match_text() — hardcoded at 0.3#
_match_text() iterates every TextCell from the layout cluster and accepts it into a predicted cell if:
intersection_over_self(text_cell_bbox, predicted_cell_bbox) > 0.3
The textcell_overlap=0.3 parameter appears in the _do_prediction_on_image_to_table signature and is passed directly when called from predict_tables . It is not exposed in any public options class — there is no field for it in TableStructureV2Options .
A known defect is documented inline : text cells can be assigned to multiple predicted cells — the first match wins, and a cell is not marked as consumed, leading to possible duplication or misassignment in adjacent cells.
get_text_in_rect() — hardcoded at 0.5#
The fallback path in both models calls get_text_in_rect() on the page backend. This method independently applies intersection_over_self > 0.5 against textline_cells . This threshold is also hardcoded with no exposure in any options class.
V1: do_matching in TFPredictor#
V1 passes tokens (PDF word-cells scaled to 2×) to TFPredictor.multi_table_predict(do_matching=self.do_cell_matching) . The matching logic itself lives in docling-ibm-models/tableformer/data_management/matching_post_processor.py, not in the Docling repo.
Configurable Surface: do_cell_matching#
The only user-accessible control is do_cell_matching: bool = True, present in both:
TableStructureOptions(V1)TableStructureV2Options(V2)
Setting do_cell_matching=False bypasses the overlap matching entirely and falls back to get_text_in_rect() for every cell. The inline comment warns this can break output when PDF cells span multiple table columns . Empirically, disabling it is worse on most corpora .
There is no option to adjust the threshold value.
Silent Data Loss#
When a PDF text cell falls below the overlap threshold for every predicted grid cell, it is silently dropped. No warning is emitted, no counter is incremented, and the cell does not appear in table_cells. Because the layout postprocessor has already transferred the text cluster into the TABLE cluster's children (removing it from regular_clusters), the dropped text also does not appear in body.children or markdown output.
Failure modes by cause#
| Cause | Effect |
|---|---|
| Predicted grid is smaller than the cluster bbox (e.g., invoice header classified as TABLE) | Cells outside every row/column band are dropped; observed 3.28% word loss on invoices, up to 53% on worst-case tables |
| Thin-lined scanned forms (entire page = one TABLE cluster) | OCR TextItems stored in texts[] but never wired into body.children; zero text in markdown output |
| Chemical structure images in table cells | Adjacent-cell bbox bleeding causes misassignment across columns |
The containment mechanism#
LayoutPostprocessor._process_special_clusters removes text clusters that overlap a TABLE bbox by >80% from regular_clusters and transfers them to the TABLE's children . Once inside the TABLE cluster, the text has only one exit route: the TableFormer grid. If the grid does not absorb it, the text is lost.
V1's _pick_orphan_cells() gap#
In docling-ibm-models, MatchingPostProcessor._pick_orphan_cells() attempts to rescue orphan PDF cells — but only when the cell falls inside both a row band and a column band of the predicted grid. A cell that matches no band is discarded silently . PR #159 in docling-ibm-models addresses the "row-band-match but no column-band" case; the no-band-at-all case remains open.
Known Proposals & Workarounds#
| Approach | Status | Reference |
|---|---|---|
recover_orphaned_table_text opt-in option (PR #3753) — re-emit unabsorbed table-region text as body text after the table | Proposed, open | |
do_cell_matching=False | Available, but empirically worse on most documents | |
TableFormerMode.FAST | Recovers some cases but quadruples loss on others | |
do_table_structure=False | Recovers 100% of text, destroys table structure | |
Post-processing: sort texts[] by (page_no, -bbox.t, bbox.l) | Client-side workaround only |
For formula-bearing table cells specifically, the plain-text fallback is unrecoverable for cells with Private Use Area (PUA) font glyphs; a proposed do_table_cell_formula_enrichment opt-in stage is under discussion .
Key Files#
| File | Role |
|---|---|
table_structure_model_v2.py | _match_text() (0.3 threshold), predict_tables() with fallback to get_text_in_rect() |
table_structure_model.py | V1 matching via TFPredictor; fallback via get_text_in_rect() |
docling_parse_backend.py | get_text_in_rect() (0.5 threshold) |
pipeline_options.py | do_cell_matching option for both V1 and V2 |
docling-ibm-models matching_post_processor.py | V1 orphan-cell rescue logic (external repo) |