Word identity
Text, page artwork, tajwīd spans and recitation timings are separate datasets built by separate pipelines. The only reason they can talk to each other is that they agree on how to name a word. This page is about that agreement — what the keys are, which dataset carries which, and what each one does and does not survive.
The obvious key, the word itself, does not work. Here is the first word of the ayah Ḥafṣ numbers 1:4, as each of our seven editions prints it:
| Riwayah | As printed | Code points |
|---|---|---|
| Ḥafṣ hafs | مَٰلِكِ | U+0645 U+064E U+0670 U+0644 U+0650 U+0643 U+0650 |
| Shuʿbah shubah | مَٰلِكِ | U+0645 U+064E U+0670 U+0644 U+0650 U+0643 U+0650 |
| Warsh warsh | مَلِكِ | U+0645 U+064E U+0644 U+0650 U+0643 U+0650 |
| Qālūn qalun | مَلِكِ | U+0645 U+064E U+0644 U+0650 U+0643 U+0650 |
| Dūrī duri | مَلِكِ | U+0645 U+064E U+0644 U+0650 U+0643 U+0650 |
| Sūsī susi | مَّلِكِ | U+0645 U+064E U+0651 U+0644 U+0650 U+0643 U+0650 |
| Bazzī bazzi | مَلِكِ | U+0645 U+064E U+0644 U+0650 U+0643 U+0650 |
Word position 10 of surah 1 in each edition’s own words array, from public/demo/fatiha.json. It is the same word in every reading — the first word of the ayah Ḥafṣ numbers 1:4 — and the same shared word number in Quran Text.
Same word. Three spellings. Even in editions where it looks identical, the code points around it can differ, so
a === b is not a test for "the same word", and neither is a normalised comparison — see
Encoding and normalisation. A word needs a name that is not its text.
The two names a word has
Our datasets use two, and they answer different questions.
surah:ayah:word | shared word number | |
|---|---|---|
| Looks like | "1:4:1" | 11 |
| Means | the n-th word of an ayah, in one edition's own numbering | one integer for this word in every edition |
| Carried by | Quran SVG Elements (data-word-key), Quran Engine, Quran Text (m.word(s, a, i)) | Quran Text (numbering, data/word-index.json) |
| Survives a change of riwayah | no — ayah numbers differ between editions | yes — that is what it is for |
| Survives a new release of the source package | yes, unless the publisher re-divides the ayah | no — it is a position in the union of the seven texts |
| Use it to | address a word on a rendered page; join to a Ḥafṣ-keyed dataset | say "this word" in more than one edition at once |
surah:ayah:word is per edition
It is the key on every word group in a split page, and it is the key you get back from a tap:
<g class="word" data-word-key="1:1:2" data-rasm-uthmani="ٱللَّهِ">
That line is from page 1 of the Elements bundle, which is a Ḥafṣ muṣḥaf. The 1 in the middle is a Ḥafṣ ayah
number. In the Warsh edition the basmalah is not a numbered ayah, so Warsh's 1:1:2 is
لِلهِ, the second word of
ࡴ۬لْحَمْدُ لِلهِ — a different word. The key is only a key
together with the edition it came from.
Nothing throws when you look up Ḥafṣ 1:4:1 in a Warsh dataset. You get a word — the first word of Warsh's fourth
ayah, which is Ḥafṣ's fifth. Persist the riwayah beside the key, exactly as you would for an ayah reference. The
ayah half of the key has every problem described in ayah-counting systems.
The shared number is per corpus
Quran Text assigns one integer to each word, dense from 1, identical in all seven muṣḥaf files. Ḥafṣ position 10 in surah 1 is number 11 in every edition, so the table above can show seven spellings of "the same word" without matching any text. Attach a translation, a root, or an audio segment to number 11 once and it is attached in all seven.
The trade is that the number is a position in the union of the seven texts. A future release of a source package
that adds or removes one word shifts every number after it, and stored rows keep resolving — to the neighbouring
word. For anything you write to a database, Quran Text also ships a content-derived key, surah:pointed#occurrence,
which looks like this on a word-index record:
{ "number": 73951, "surah": 72, "index": 153, "key": "72:لو#1", "status": "word_boundary" }
72:لو#1 means "the first occurrence in surah 72 of the word whose pointed skeleton is لو". It survives a rebuild
because it is derived from the text, not from a count.
Inside a muṣḥaf file, every array named *_starts holds a position — an index into this edition's
words. Only the numbering block holds the shared number. Surah 112 begins one position earlier in the
Warsh file than in the Ḥafṣ file, because Warsh prints one word fewer before it; the shared number of its first
word is the same in both. Mix the two and your text is off by a word or two, with no error.
Joining across blocks
The practical recipe is: rendered pages speak surah:ayah:word in Ḥafṣ; the text dataset translates that into
the shared number; everything else hangs off the number.
From a tap to a key
On an Elements page, the key is on the group you tapped. This is the code the demo on this site runs, trimmed.
svg.addEventListener("click", (e) => { const word = e.target.closest("g.word"); if (!word) return; const key = word.dataset.wordKey; // "1:1:2" — Ḥafṣ numbering, because the page is Ḥafṣ });From a key to the shared number
Quran Text's word index resolves Ḥafṣ
surah, ayah, positionto the number in one lookup.const idx = await WordIndex.load("data/word-index.json"); idx.find(1, 4, 1).number; // 11The
hafsfield on every word-index record —{ surah, ayah, position }in the Kufan count — exists so that a word-level dataset keyed on Ḥafṣ coordinates, which most existing ones are, joins in a single lookup.From the number to any edition
const w = hafs.word(1, 4, 1); w.number; // 11 w.to(warsh).text; // مَلِكِ w.to(susi).text; // مَّلِكِ
Elements and Quran Text both count 77,432 words in the Ḥafṣ muṣḥaf, and both derive word boundaries from the
publisher's typesetting — but separately. Elements' own format notes say its boundaries agree with other
word-by-word decompositions almost everywhere and that a handful of words are split differently. So a
surah:ayah:word key from one is almost always the same word in the other. If you join them, spot-check
rather than assume, and treat a mismatch as a finding to report, not a bug to patch locally.
What is not a word key
- The Arabic string. Encodings differ between datasets built from the same publisher package, and spellings
differ between editions. Matching on text is the bug to avoid: the word key is
surah:ayah:word(Engineering, rule 2.2). - An index into
wordsfrom a different edition. Positions are per file, and a suffix carries one meaning (Naming, rule 4.3), so anindexis never anumberand never akey. - Quran SVG's
id="verse-N". That is a global ayah counter across all five vectorised muṣḥafs, not a word and not stable per muṣḥaf; the Quran SVG reference says to readsurahandayahfrom the polygon instead. - A tajwīd span. Spans are character offsets into one edition's text, not word references at all — see Annotation layers.
Click a word on the real page and see its key, its five text forms and its box — everything hangs off
data-word-key.
How this data is made
No publisher package supplies word-level data, so the word boundaries in Quran Text are derived from the
publisher's own typesetting and the shared numbering is built by aligning the seven texts on the bare letter
skeleton of each word; the build asserts on every file that words.length + joined + missing == numbering.total.
Elements assigns ink to words from the vectorised artwork and checks that every data-word-key in an index
resolves to a word group on the page it names. Neither cross-checks the other today.