Skip to content

Search & verification

The Roman fold

3 min readUpdated Verified against 7a343c6 on 2026-09-25

Page actions

ChatGPT and Claude are third-party services. The link gives them this page's public address and a short instruction. This site records nothing.

roman_norm: the five-step phonetic fold that lets waheguru and vaahiguroo meet at one key, why it must be byte-identical in three repositories, and the 24,719 golden vectors that hold it there.

Seekers spell a Gurmukhi word in Roman letters the way they hear it. The fold makes every spelling of a word land on one key — and it must be exactly the same fold wherever it runs, or the key the query produces will not match the key in the index and lines go missing without any error.

The fold in three places Seekers spell the same word many ways. roman_norm folds a Roman word in five steps — letter substitutions, digraphs, voicing pairs, the y glide, vowels dropped and doubles collapsed — into a key such as vhgr. The data repository applies it when it builds the translit_norm column; the platform applies it at query time in search and verification; the iOS app carries a Swift port. The contract file golden_roman_norm.ndjson records 24,719 input and output pairs generated from the platform, and every repository asserts against it. 06 · The fold in three places roman_norm turns many spellings into one key — and it lives in threerepositories that 24,719 golden vectors hold byte-identical One fold, five stepsThree homes, one contract Many spellings, one word — waheguru · vaahiguroo · wahegurooh → the same keyMany spellings, one wordwaheguru · vaahiguroo · wahegurooh → the same keyIf the two folds ever differ — search breaks silently: no error, just missing linesIf the two folds ever differsearch breaks silently: no error, just missing lines1 · Substitute letters — w→v z→j q→k x→k1 · Substitute lettersw→v z→j q→k x→k2 · Digraphs — sh, ch, kh, th … → one letter2 · Digraphssh, ch, kh, th … → one letter3 · Voicing pairs — b→v k→g t→d p→v3 · Voicing pairsb→v k→g t→d p→v4 · The y glide — gyan ~ giaan4 · The y glidegyan ~ giaan5 · Vowels and doubles — vowels dropped, doubles collapse5 · Vowels and doublesvowels dropped, doubles collapseThe key — vhgr · jsd · krsn · gnThe keyvhgr · jsd · krsn · gnHome 1 · sggs-data — at build time — sggs_pipeline.pyHome 1 · sggs-dataat build timesggs_pipeline.pyHome 2 · platform — at query time — romannorm.pyHome 2 · platformat query timeromannorm.pyHome 3 · the app — on the device — RomanNorm.swiftHome 3 · the appon the deviceRomanNorm.swiftcontract/golden_roman_norm.ndjson — 24,719 input → output pairs, generated from home 2contract/golden_roman_norm.ndjson24,719 input → output pairs, generated from home 2Checked everywhere — make contract · the app’s vendor lock · byte-parity testsChecked everywheremake contract · the app’s vendor lock · byte-parity tests component in this projectproven, verified statepin: a reviewed lock fileexplanationflow: data or control moves this wayreference: pins, reads or derives from Source of truth: webapp/romannorm.py · contract/golden_roman_norm.ndjson· tools/gen_golden_vectors.py · contract/_meta.json v1.3.10 · verified 2026-09-27 · d85c2adSri Guru Granth Sahib Ji — Knowledge Base · engineering wiki
Poster 06 — The fold in three places: the five steps of roman_norm, its three homes, and the golden vectors that bind them · open full size

The function, read from the source

webapp/romannorm.py · roman_norm · lines 11–28
def roman_norm(s):
"""Phonetic-fold Roman normal form, applied to BOTH the index column and the
query: 'waheguru'/'vaahiguroo' -> 'vhgr'; 'yashoda'/'jasodaa' -> 'jsd';
'krishna'/'krisan' -> 'krsn'; 'gyan'/'giaan' -> 'gn'.
KEEP IN SYNC with sggs-data pipeline/sggs_pipeline.py:roman_norm()."""
out = []
for w in s.lower().split():
w = w.replace('w', 'v').replace('z', 'j').replace('q', 'k').replace('x', 'k')
for dg in ('sh', 'chh', 'ch', 'kh', 'gh', 'jh', 'th', 'dh', 'bh', 'ph', 'rh', 'f'):
w = w.replace(dg, dg[0] if dg != 'f' else 'p')
w = w.replace('b', 'v').replace('k', 'g').replace('t', 'd').replace('p', 'v') # ਬ/ਵ + Sanskrit↔Punjabi voicing (bhakti~bhagatee)
if w.startswith('y'): w = 'j' + w[1:]
w = w.replace('y', '') # medial glide: gyan ~ giaan
head = w[0] if w and w[0] in 'aeiou' else ''
body = re.sub('[aeiou]', '', w)
body = re.sub(r'(.)\1+', r'\1', body) # collapse doubles
out.append((head + body) if (head + body) else w)
return ' '.join(o for o in out if o)

Read from the source at build time — view these lines on GitHub at c3d9360.

Source: webapp/romannorm.py, the platform’s one copy of the fold.

In words, for each lower-cased word:

  1. Substitute letters — w→v, z→j, q→k, x→k.
  2. Reduce digraphs — sh, chh, ch, kh, gh, jh, th, dh, bh, ph, rh become their first letter; f becomes p.
  3. Merge voicing pairs — b→v, k→g, t→d, p→v (ਬ/ਵ, and Sanskrit↔Punjabi voicing: bhakti ~ bhagatee).
  4. The y glide — a leading y becomes j; a medial y is dropped (gyan ~ giaan).
  5. Vowels and doubles — a leading vowel is kept, every other vowel is dropped, doubled letters collapse.

So waheguru, vaahiguroo → vhgr; yashoda, jasodaa → jsd; krishna, krisan → krsn; gyan, giaan → gn. The fold is lossy on purpose: it trades precision for recall, which is why the waterfall tries it only after the exact tiers, and why the verification engine uses it to find candidates, never to decide a verdict on its own.

Three homes

WhereFileWhen it runs
sggs-datapipeline/sggs_pipeline.py:roman_normat build time, writing lines.translit_norm
sggs-platformwebapp/romannorm.py (re-exported by serve.py, imported by verify.py)at query time: search tier 11, the verification engine
gurbani-soul-iosGurbaniSearchKit/Sources/GurbaniSearchKit/RomanNorm.swifton the device: search and verification

verify.py cannot import serve.py (serve imports verify), which is why the fold lives in its own module; there is exactly one copy in this repository.

One contract

tools/gen_golden_vectors.py drives the platform’s function over a corpus-wide vocabulary and records 24,719 input → output pairs in contract/golden_roman_norm.ndjson. make contract regenerates the vectors and fails if the committed file drifts; the app pins the file by sha256 in its vendor.lock.json and runs byte-parity tests against the Swift port; the data repository’s copy is held to the same vectors. Change one character of the fold and three test suites fail before any index goes stale. How the contract is generated and replayed is on Harnesses and golden vectors.

What the fold is not

  • Not scripture, not a transliteration. translit is the reading aid; translit_norm is an index key that is never shown to a reader.
  • Not applied to Gurmukhi. A Gurmukhi query goes to the text and skeleton columns; the fold exists for Roman input only.
  • Not a place for clever fixes. A “small improvement” to the fold changes the key of every line in the corpus: rebuild the database, regenerate the contract, re-vendor the app.