Spans and Offsets
Nothing in context-grid is identified by a chunk ID. IDs change the moment you change the
chunker — which is the one thing this tool exists to compare. Instead, every piece of text
knows exactly which characters of which document it came from. That’s a Span, and it’s the
one idea the whole package rests on.
Span
Span(doc_id: str, start: int, end: int)A half-open range [start, end) — document.text[start:end] is the literal slice, no
off-by-one to remember. start must be >= 0 and end must be >= start, or construction
raises cg.SpanError.
Seeing it on a real chunk
This page reads one file, ./mydocs/refund.md. Create it first:
from pathlib import Path
Path("mydocs").mkdir(exist_ok=True)Path("mydocs/refund.md").write_text( "# Refund policy\n\n" "Customers can request a refund within 14 days of delivery. Outside that window,\n" "we only offer store credit. Contact support to request one.\n\n" "Email support@example.com with your order number.\n")import contextgrid as cg
corpus = cg.Corpus.from_dir("./mydocs")parser = cg.get_parser("markdown")parsed = parser.parse(corpus.require("refund.md"))
chunker = cg.get_chunker("recursive:20")for c in chunker.chunk(parsed): print(c.id, c.span, repr(c.text[:30])) assert parsed.text[c.span.start:c.span.end] == c.textrefund.md:0-96 Span('refund.md', 0, 96) '# Refund policy\n\nCustomers can'refund.md:89-156 Span('refund.md', 89, 156) 'window,\nwe only offer store cr'refund.md:152-207 Span('refund.md', 152, 207) 'one.\n\nEmail support@example.co'Every Chunk carries .span (the Span), plus .doc_id, .char_start, .char_end, and
.char_length as shortcuts onto it. The assert above is the guarantee itself: for a chunk
with offsets_exact=True, its .text is always exactly parsed.text[span.start:span.end].
offsets_exact: the honesty flag
Both ParsedDocument and Chunk carry offsets_exact: bool = True. It’s true when the
text is a literal slice of what it claims to span. Most parsers and most chunkers keep it
true. Some can’t:
StructuralChunker(keep_heading_path=True)prepends the heading chain to a chunk’s text ("Refund policy\n\n..."), so the chunk is no longer a literal slice of the document — it setsoffsets_exact=Falseon those chunks. Every other structural-chunker chunk keeps it true.- The four “paid” ingestion strategies that rewrite what gets indexed —
contextual,hypothetical-questions,propositions,summary— setoffsets_exact=Falseon the indexed text they generate. The retrievable side (what a hit actually returns) keeps real offsets; only the rewritten, indexed side is approximate. - A parser that reflows columns, reorders reading sequence, or rewrites table cells sets
offsets_exact=Falseon the wholeParsedDocumentit returns, and every chunk cut from it inherits that.
structural = cg.get_chunker("structural:200,keep_heading_path=true")for c in structural.chunk(parsed): print(c.offsets_exact, repr(c.text[:40])) print("still a literal slice:", parsed.text[c.span.start:c.span.end] == c.text)False '# Refund policy\n\n# Refund policy\n\nCusto'still a literal slice: FalseThe span helper functions
Span itself has overlap arithmetic (.overlap_len(), .intersection(), .contains(),
.iou(), .coverage_of()), and the module has free functions for sets of spans — this is
what scoring is built on, turning “did the retrieved chunks cover the gold evidence” into a
character count rather than a chunk-ID lookup:
a = cg.Span("refund.md", 0, 50)b = cg.Span("refund.md", 40, 90)
print("overlap_len:", a.overlap_len(b))print("merge_spans:", cg.merge_spans([a, b]))print("coverage_fraction:", cg.coverage_fraction(cg.Span("refund.md", 0, 100), [a, b]))overlap_len: 10merge_spans: [Span('refund.md', 0, 90)]coverage_fraction: 0.9merge_spans(spans)— collapses overlapping and touching spans into a minimal disjoint set, grouped by document, in reading order.total_length(spans)— characters covered by a set of spans, counting shared characters once.covered_length(target, others)/coverage_fraction(target, others)— how much oftargetis covered byothers, as a count or a fraction in[0, 1]. This is the core of union recall: a gold span split across two retrieved chunks is fully covered when both come back, even though neither alone clears a per-chunk threshold.intersection_length(left, right)— characters covered by both sets, each side merged first so overlapping chunks on either side don’t inflate the count. What character-level precision and recall are built from — see Metrics.