@@ -227,6 +227,7 @@ def predict_tables(
227227 for table_cluster , tbl_box in in_tables :
228228 # Check if word-level cells are available from backend:
229229 sp = page ._backend .get_segmented_page ()
230+ using_word_cells = False
230231 if sp is not None :
231232 tcells = sp .get_cells_in_bbox (
232233 cell_unit = TextCellUnit .WORD ,
@@ -235,6 +236,8 @@ def predict_tables(
235236 if len (tcells ) == 0 :
236237 # In case word-level cells yield empty
237238 tcells = table_cluster .cells
239+ else :
240+ using_word_cells = True
238241 else :
239242 # Otherwise - we use normal (line/phrase) cells
240243 tcells = table_cluster .cells
@@ -284,6 +287,77 @@ def predict_tables(
284287 .get ("rs_seq" , [])
285288 )
286289
290+ # docling-side workaround for the edge case, main root cause is in TFPredictor
291+ # model doesn't see the last row because it's at the very bottom edge of the crop
292+ if (
293+ self .do_cell_matching
294+ and using_word_cells
295+ and tokens
296+ and table_cells
297+ ):
298+ matched_ids = {
299+ str (k )
300+ for k in table_out ["predict_details" ]
301+ .get ("matches" , {})
302+ .keys ()
303+ }
304+ cells_bottom = max (
305+ (tc .bbox .b for tc in table_cells if tc .bbox is not None ),
306+ default = 0.0 ,
307+ )
308+ unmatched = []
309+ if cells_bottom > 0.0 :
310+ for tok in tokens :
311+ if str (tok ["id" ]) in matched_ids :
312+ continue
313+ tok_bbox = BoundingBox .model_validate (
314+ tok ["bbox" ]
315+ ).scaled (1 / self .scale )
316+ if (
317+ tok_bbox .t > cells_bottom
318+ and tok_bbox .b <= table_cluster .bbox .b
319+ ):
320+ unmatched .append ((tok , tok_bbox ))
321+ if len (unmatched ) > 1 :
322+ centres = sorted ((tb .t + tb .b ) / 2.0 for _ , tb in unmatched )
323+ median_centre = centres [len (centres ) // 2 ]
324+ avg_height = sum (tb .b - tb .t for _ , tb in unmatched ) / len (
325+ unmatched
326+ )
327+ coherent = all (
328+ abs ((tb .t + tb .b ) / 2.0 - median_centre ) <= avg_height
329+ for _ , tb in unmatched
330+ )
331+ if not coherent :
332+ unmatched = []
333+ if unmatched :
334+ extra_row_idx = num_rows
335+ num_cols = max (num_cols , 1 )
336+ for col_idx , (tok , tok_bbox ) in enumerate (unmatched ):
337+ table_cells .append (
338+ TableCell (
339+ text = tok ["text" ],
340+ bbox = tok_bbox ,
341+ row_span = 1 ,
342+ col_span = 1 ,
343+ start_row_offset_idx = extra_row_idx ,
344+ end_row_offset_idx = extra_row_idx + 1 ,
345+ start_col_offset_idx = col_idx % num_cols ,
346+ end_col_offset_idx = (col_idx % num_cols ) + 1 ,
347+ column_header = False ,
348+ row_header = False ,
349+ row_section = False ,
350+ )
351+ )
352+ num_rows += 1
353+ _log .debug (
354+ "Recovered %d unmatched token(s) into extra row %d "
355+ "for table cluster id=%d" ,
356+ len (unmatched ),
357+ extra_row_idx ,
358+ table_cluster .id ,
359+ )
360+
287361 tbl = Table (
288362 otsl_seq = otsl_seq ,
289363 table_cells = table_cells ,
0 commit comments