unstructured
e2732cf0 - fix: stop treating every hyphen as a bullet delimiter in text partitioning (#4458)

Commit
9 days ago
fix: stop treating every hyphen as a bullet delimiter in text partitioning (#4458) ## Summary `-` (U+002D) and `–` (U+2013) are bullet glyphs *and* ordinary punctuation — an intra-word hyphen, an intra-number separator, a minus sign. `UNICODE_BULLETS` was reused as an **unanchored split delimiter**, so `partition_text` split at every hyphen in a qualifying line *and consumed the hyphen itself*: ```python partition_text(text="Phone: 555-123-4567\n\nEnd") # ['Phone: 555', '123', '4567', 'End'] partition_text(text="Contact: Jean-Luc Picard\n\nEnd") # ['Contact: Jean', 'Luc Picard', 'End'] ``` The only thing limiting the blast radius was the `all_lines_short` guard, which counts **space-separated tokens** — so long English prose escaped by accident of word count, while short `Field: value` lines, log lines, YAML and all CJK text (no inter-word spaces, so the count is always 1) did not. This happened at partition time, upstream of every transform, on a job that reported success. Nothing downstream recovered it: both chunkers rejoin elements with a blank line, so the fragments never became contiguous again — a regex for a phone number or an SSN can never match across an element boundary. Scope: `FileType.TXT` and its 18 extensions (`.txt .text .c .cc .cpp .cs .cxx .go .java .js .log .php .py .rb .swift .ts .yaml .yml`) plus `.eml`/`.msg`, which delegate to `partition_text`. PDF/DOCX/PPTX/HTML are unaffected — verified by call graph and by round-tripping the same content through DOCX and HTML. ## The fix Two conditions now have to hold before an ambiguous glyph is read as a bullet, and **three** call sites needed changing, not one. **1. Start of line.** `PARAGRAPH_PATTERN_RE` splits only on the unambiguous glyphs. Dash bullets at line start were already handled by the `\n` branch, so nothing is lost. **2. Followed by whitespace** (or end of text). A bullet is separated from the item it introduces; a sign is not. Without this, `-123.45` reads as a bullet and is rewritten to `123.45` — **silently flipping the value's sign**. `E_BULLET_PATTERN` in the same file already required `(?=\s)` for exactly this reason, so this brings dash handling in line with existing precedent rather than inventing a convention. Applied to: - `PARAGRAPH_PATTERN_RE` — the paragraph split. - `BULLET_SPLIT_RE_0W` (new) — used by `group_bullet_paragraph`, which splits on `UNICODE_BULLETS_RE_0W`. This site is reached for any paragraph that *starts* with a bullet, i.e. exactly a dash bullet list, so without it a genuine dash list whose items contain hyphens was still shredded. - `UNICODE_BULLETS_RE` — backs `clean_bullets`, `is_bulleted_text` and `_is_empty_bullet`. Fixing only the split sites preserves the line boundary but lets classification strip the sign anyway. `-` and `–` stay in `UNICODE_BULLETS`; the qualification is scoped to the ambiguous glyphs via a separate alternation, so `•item` and friends need no separator and are unchanged in every respect. ### Before / after ``` 'Phone: 555-123-4567' ['Phone: 555','123','4567'] -> ['Phone: 555-123-4567'] 'Contact: Jean-Luc Picard' ['Contact: Jean','Luc Picard'] -> ['Contact: Jean-Luc Picard'] 'trace_id: 550e8400-e29b…' 5 fragments -> 1 element 'Content-Type: image/jpeg' ['Content','Type: image/jpeg'] -> ['Content-Type: image/jpeg'] '-123.45' ListItem('123.45') # sign flipped -> Text('-123.45') '- readings\n-5\n-10 °C' 3 sign-stripped items -> ListItem('readings -5 °C -10 °C') ``` ## Behavior changes Both are declared in the changelog. 1. An inline dash bullet list on a single line (`- one - two - three`) no longer splits into separate elements. Ambiguous input either way, and a far better trade than destroying every hyphenated identifier. 2. A dash with no separating whitespace (`-item`) is no longer a bullet: it keeps its leading character and is no longer classified as a `ListItem`. Unchanged: `- item` at line start is still a bullet, a lone `-` is still an empty bullet, `----` is still not a bullet, and inline `•`-style bullets still split. ## Verification **1828 tests pass** — cleaners, nlp, text, email, msg, text_type, partition/common, chunking, auto, html, docx, md, documents. Nothing broke. **Corpus audit** over the 66 `.txt`/`.eml` files in `example-docs/`: | | elements | dash chars recovered | docs with text loss | | -- | -- | -- | -- | | before | 26,857 | — | — | | after | 26,524 | **+430** | **0** | The recovered characters are the point: the old split *deleted* the delimiter. Among them, wrapped URL slugs in `norwich-city.txt` were losing their leading dash (`-can-get-hucks-firing-1-648688`). **Adversarial cases** checked against the bullet regex: `-Xmx512m`, `--flag`, `-o`, `-.5`, `-1e9`, `-5`, `-item`, `----` (none are bullets); `- item`, `– item`, `-\titem`, `-`, `-\nnext`, `•item`, `○x`, `* item` (all are). **New regression tests** cover hyphenated values through `partition_text` and a new `.eml` fixture (Japanese phone, US SSN, ISO date, card, IBAN, UUID, hyphenated personal name, YAML value, log line with hostname); dash/en-dash/indented bullet lists; dash list items containing hyphens; inline `•` bullets; the Apache-License short-line case cited in `group_broken_paragraphs`; signs (`-123.45`, `–10 °C`, `-5`); and both declared behavior changes, pinned with a comment saying they are intentional. ## Notes for review Two related defects found during this work, deliberately **not** in scope: 1. `layout_list_to_list_items` (`partition/common/common.py`) has the same unanchored split on `UNICODE_BULLETS_RE`. Currently unreachable — the live `pdf.py` hi_res path passes `infer_list_items=False` and the `ocr_only` path never emits `ElementType.LIST` — but latent. 2. `all_lines_short` in `group_broken_paragraphs` counts space-separated tokens and is structurally blind to CJK, where the count is always 1. This PR makes it moot for dashes; it stays wrong for any other ambiguous delimiter. `paragraph_grouper=False` was the only workaround, and it is not reachable from the platform — the parameter is absent from the partition node's settings schema and from `unstructured_client`'s `PartitionParameters`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/Unstructured-IO/unstructured/pull/4458?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: ryannikolaidis <1208590+ryannikolaidis@users.noreply.github.com> Co-authored-by: badGarnet <badGarnet@users.noreply.github.com>
Author
Parents
Loading