diff --git a/docling/utils/deepseekocr_utils.py b/docling/utils/deepseekocr_utils.py
index 76a39aaca5..01c94bcdb2 100644
--- a/docling/utils/deepseekocr_utils.py
+++ b/docling/utils/deepseekocr_utils.py
@@ -158,23 +158,16 @@ def _collect_annotation_content(
break
ii += 1
else:
- # Original logic for other labels
+ # Text and captions can span lines and paragraphs within one annotation.
while i < len(lines):
- content_line = lines[i].strip()
- if content_line:
- if re.match(annotation_pattern, content_line):
- break
- visited_lines.add(i)
+ if re.match(annotation_pattern, lines[i].strip()):
+ break
+ if i not in visited_lines:
content_lines.append(lines[i].rstrip())
- i += 1
- if label_str not in ["figure", "image"]:
- break
- else:
- i += 1
- if content_lines:
- break
+ visited_lines.add(i)
+ i += 1
- return "\n".join(content_lines), i
+ return "\n".join(content_lines).strip("\n"), i
def _process_annotation_item(
diff --git a/tests/test_ocr_multiline_blocks.py b/tests/test_ocr_multiline_blocks.py
new file mode 100644
index 0000000000..2cfb589c16
--- /dev/null
+++ b/tests/test_ocr_multiline_blocks.py
@@ -0,0 +1,60 @@
+# SPDX-FileCopyrightText: The Docling Contributors
+# SPDX-License-Identifier: MIT
+
+import pytest
+from docling_core.types.doc import DocItemLabel, Size
+
+from docling.utils.deepseekocr_utils import (
+ parse_deepseekocr_markdown,
+ parse_unlimited_ocr_markdown,
+)
+
+
+@pytest.mark.parametrize("parser", ["deepseek", "unlimited"])
+def test_text_block_keeps_continuation_lines_and_following_table(parser: str) -> None:
+ def marker(label: str, top: int) -> str:
+ if parser == "deepseek":
+ return f"{label}[[100,{top},900,{top + 100}]]\n"
+ return f"<|det|>{label} [100,{top},900,{top + 100}]<|/det|>"
+
+ source = (
+ marker("text", 100)
+ + "First line\nSecond line\n\nThird paragraph\n"
+ + marker("table", 400)
+ + "
\n"
+ + marker("table_caption", 600)
+ + "Caption first line\nCaption second line\n"
+ + marker("text", 800)
+ + "Final block"
+ )
+ parse = (
+ parse_deepseekocr_markdown
+ if parser == "deepseek"
+ else parse_unlimited_ocr_markdown
+ )
+ doc = parse(source, Size(width=600, height=800), page_no=2)
+ text = next(item for item in doc.texts if item.label == DocItemLabel.TEXT)
+ assert text.text == "First line\nSecond line\n\nThird paragraph"
+ assert text.prov[0].page_no == 2
+ assert len(doc.tables) == 1
+ assert doc.tables[0].data.grid[0][0].text == "Total"
+ caption = doc.tables[0].captions[0].resolve(doc)
+ assert caption.text == "Caption first line\nCaption second line"
+ assert doc.texts[-1].text == "Final block"
+ assert "Second line" in doc.export_to_markdown()
+
+
+def test_caption_does_not_claim_table_markup_already_collected() -> None:
+ source = (
+ "table[[100,100,900,300]]\n"
+ "table_caption[[100,400,900,500]]\nCaption first\nCaption second\n\n"
+ "\n\n"
+ "text[[100,600,900,700]]\nFinal block"
+ )
+ doc = parse_deepseekocr_markdown(source, Size(width=600, height=800), page_no=1)
+ assert doc.tables[0].data.grid[0][0].text == "Total"
+ assert (
+ doc.tables[0].captions[0].resolve(doc).text == "Caption first\nCaption second"
+ )
+ assert all("" not in item.text for item in doc.texts)
+ assert doc.texts[-1].text == "Final block"