diff --git a/docling/models/stages/reading_order/readingorder_model.py b/docling/models/stages/reading_order/readingorder_model.py index 8ae627ba25..cc60d2655f 100644 --- a/docling/models/stages/reading_order/readingorder_model.py +++ b/docling/models/stages/reading_order/readingorder_model.py @@ -670,11 +670,18 @@ def _merge_elements(self, element, merged_elem, new_item, page_height): bbox=merged_elem.cluster.bbox.to_bottom_left_origin(page_height), ) continuation_text = merged_elem.text.lstrip() - if new_item.text.endswith("\u00ad") or ( + # A hard hyphen only splits a word when it is attached to that word + # ("algo-" / "rithms"). A hyphen following whitespace is a literal + # character -- a dangling dash, a wrapped flag or a minus sign -- and + # must not be swallowed. A soft hyphen (U+00AD) is always a split marker. + hard_hyphen_split = ( new_item.text.endswith("-") + and len(new_item.text) > 1 + and new_item.text[-2].isalnum() and continuation_text and continuation_text[0].islower() - ): + ) + if new_item.text.endswith("\u00ad") or hard_hyphen_split: # A soft hyphen or hard-hyphenated lowercase continuation is a split word. new_item.text = new_item.text[:-1] + merged_elem.text new_item.orig = ( diff --git a/tests/test_readingorder_hyphenated_merges.py b/tests/test_readingorder_hyphenated_merges.py index cde8eadc62..40b89c4cc8 100644 --- a/tests/test_readingorder_hyphenated_merges.py +++ b/tests/test_readingorder_hyphenated_merges.py @@ -57,6 +57,10 @@ def _element(cluster_id: int, text: str) -> TextElement: ("algo-", "rithms", "algorithms"), ("algo\u00ad", "rithms", "algorithms"), ("algo-", "Rithms", "algo- Rithms"), + # A hyphen following whitespace is a literal character, not a word split, + # so it is kept and the elements are joined with a space. + ("run -", "prio now", "run - prio now"), + ("the value is -", "five units", "the value is - five units"), ], ) def test_merge_elements_dehyphenates_lowercase_continuations(