Repository navigation
Span Tracking
When you segment text, downstream tools often need to highlight the original document or PDF. If you clean the text first (stripping HTML, fixing line breaks), character positions shift.
We solve this using two complementary mechanisms:
-
PUA Sentinel Masking: 1:1 character replacements for zero-drift disambiguation (
clean=False). -
Cumulative Delta Tracking (
OffsetMap): An$O(\log K)$ binary search map that projects coordinates from cleaned text back to raw source text (clean=True).
In standard mode (clean=False), cleave-sbd never modifies the length of the string buffer:
Periods and punctuation inside abbreviations, numbers, quotes, and ellipses are temporarily substituted 1:1 with single-character Unicode Private Use Area (PUA) codepoints (\ue000–\ue021):
| Punctuation Type | Character | PUA Sentinel | Codepoint |
|---|---|---|---|
| Standard Period (Abbr / Honorific) | . |
\ue000 |
PUA_PERIOD |
| CJK Fullwidth Period | 。 |
\ue001 |
PUA_CJK_PERIOD |
| Fullwidth Period | . |
\ue002 |
PUA_FULLWIDTH_PERIOD |
| Exclamation Mark | ! |
\ue004 |
PUA_EXCLAMATION |
| Question Mark | ? |
\ue005 |
PUA_QUESTION |
| Ellipsis Dot | . |
\ue020 |
PUA_ELLIPSIS_DOT |
Because every substitution preserves length 1:1, slice coordinates (start, end) extracted from masked_text map directly to original_text[start:end]. Once boundaries are identified, unmask_all() restores sentinels in a single C-level .translate() pass.
When you enable clean=True, normalizers strip HTML tags, fix quote formatting, and repair broken OCR/PDF line breaks. These operations change string length.
To preserve original coordinates, we track every length modification in an immutable OffsetMap:
Raw Source Text (Length = 39):
[0] [38]
v v
"<p>Hello world! This is a test sentence.</p>"
Transformations Applied:
1. Strip '<p>' at raw [0:3] -> Net shift: +3 chars deleted
2. Strip '</p>' at raw [35:39] -> Net shift: +4 chars deleted (Total cumulative delta: +7)
Cleaned Text Buffer (Length = 32):
[0] [31]
v v
"Hello world! This is a test sentence."
OffsetMap Internal Arrays:
clean_keys: ( 0, 32 )
cum_deltas: ( 3, 7 )
When sentence boundaries are found in the cleaned buffer, OffsetMap.clean_span_to_raw_span() projects them back to raw coordinates in
# Project Clean Span [0:12] ("Hello world!")
raw_start = offset_map.clean_to_raw(0) # 0 + 3 = 3 (points to 'H' in raw text)
raw_end = offset_map.clean_to_raw(12) # 12 + 3 = 15 (points to '!' in raw text)
# -> Raw Span: [3:15] maps directly to: "<p>[Hello world!] This is..."
# Project Clean Span [13:32] ("This is a test sentence.")
raw_start = offset_map.clean_to_raw(13) # 13 + 3 = 16 (points to 'T' in raw text)
raw_end = offset_map.clean_to_raw(32) # 32 + 7 = 39 (points to end of raw text)
# -> Raw Span: [16:39] maps directly to: "...[This is a test sentence.]</p>"When char_span=True is enabled, sentences are returned as immutable, slotted TextSpan objects:
@dataclass(slots=True, frozen=True)
class TextSpan:
sent: str # Normalized sentence string (for LLMs / NLP models)
start: int # Raw original document start index (for UI highlights)
end: int # Raw original document end index (for UI highlights)
clean_start: int | None # Cleaned buffer start index (optional)
clean_end: int | None # Cleaned buffer end index (optional)
raw_slice: str | None # Raw uncleaned slice text[start:end] (optional)This guarantees:
-
Downstream NLP models & LLMs receive clean, normalized sentence text via
span.sent. -
Frontend UI renderers & PDF viewers receive exact source coordinates (
span.start,span.end) to paint highlights onto the original document without offset drift.