Protected text

Automatic protection covers URLs, email addresses, and semantic-version-like strings. Caller-defined spans can protect additional source ranges. High- confidence literal promotion is opt-in:

result = prepare("See https://example.org/a2 and v1.2.3", language="en", normalize_literals=True)

With normalize_literals=True, structured URL, e-mail, version, and contextual Roman candidates are rendered before generic stages. Caller-defined spans are still absolute and always win over promotion.

from spokenform import ProtectedSpan, prepare

text = "Keep Dr. literal, but verbalize 12."
start = text.index("Dr.")
result = prepare(
    text,
    language="en",
    protected_spans=[ProtectedSpan(start, start + 3)],
)

Tuple pairs such as (start, end) are also accepted. Ranges use Python string offsets and are half-open: the start is included and the end is excluded.

Invalid or overlapping caller ranges are skipped with warnings by default. strict=True turns them into ProtectionError.

Protection is fail-closed for structured expressions: if a caller span partially intersects a recognized numeric quantity, the complete candidate is protected so later generic-number or abbreviation stages cannot create a hybrid rewrite. URLs, e-mail addresses, versions, numbers, units, abbreviations, and adjacent unprotected expressions are covered by the adapter tests.