What a developer needs to know first
Four ideas that are unlike anything in ordinary text software. Each of them fails without an exception or a red test; the reader just sees the wrong thing.
The shape of the thing
The Qur’an is one fixed text. It does not have editions in the sense a novel does, and none of what follows is about the text changing. All four ideas are about how the same unchanged text is divided, transmitted, printed and encoded, and each of those is a separate axis your code has to keep straight.
Surah numbers are the one part you can rely on completely.
Idea 1 — An ayah number depends on who is counting
Early scholars in different centres divided the identical text into verses at slightly different points. Not different words; different placement of the numbered breaks. Six of those traditions are canonical, and they give six different totals.
| System | Arabic | Total ayat | Associated qurrāʾ |
|---|---|---|---|
| First Madinan madani-first | المدني الأول | 6,214 | abu-jafar |
| Last Madinan madani-last | المدني الأخير | 6,214 | nafi |
| Makkan makki | المكي | 6,219 | ibn-kathir |
| Basran basri | البصري | 6,204 | abu-amr, yaqub |
| Damascene dimashqi | الدمشقي | 6,226 | ibn-amir |
| Kufanreference kufi | الكوفي | 6,236 | asim, hamza, kisai, khalaf |
Read at build time from public/demo/qiraat.json, which scripts/sync-demo-data.mjs built from the Qiraat Ayah Map repository at a pinned commit.
It is the Kufan total. Ḥafṣ follows the Kufan tradition, and Ḥafṣ is what most of the world prints, so the number looks universal. Hard-code it and your app is correct until the first user switches reading, then it truncates or overruns.
First and Last Madinan both total 6,214 and are still different systems: their boundaries differ in places that cancel out in the sum. You cannot identify a counting system by its total.
What to do: read the count from the data you loaded, because numbering systems differ (Quranic text, rule 5.2); store the counting system alongside any reference you persist, as the ayah key asks (Engineering, rule 2.1); and translate between systems with Qiraat Ayah Map rather than arithmetic. Full detail in Ayah-counting systems.
Idea 2 — There is more than one transmitted reading
This is the selector in our APIs. We ship seven:
| Key | Riwayah | Counting system | Ayat in this muṣḥaf | System total |
|---|---|---|---|---|
| hafs | Ḥafṣ ʿan ʿĀṣim | Kufi | 6,236 | 6,236 |
| shubah | Shuʿbah ʿan ʿĀṣim | Kufi | 6,236 | 6,236 |
| warsh | Warsh ʿan Nāfiʿ | Last Madani | 6,214 | 6,214 |
| qalun | Qālūn ʿan Nāfiʿ | Last Madani | 6,214 | 6,214 |
| duri | al-Dūrī ʿan Abī ʿAmr | First Madani | 6,217 | 6,214differs |
| susi | al-Sūsī ʿan Abī ʿAmr | First Madani | 6,218 | 6,214differs |
| bazzi | al-Bazzī ʿan Ibn KathīrBeta | Makki | 6,220 | 6,219differs |
Muṣḥaf counts come from quran-text’s own files; system totals from qiraat-ayah-map. Both are read at build time. Where they differ, both numbers are correct about different questions — see below.
The differs marks in the last column are the second trap:
The published total of a counting system is a scholarly position. The ayah count of a particular printed muṣḥaf is what a publisher set in type. The al-Sūsī muṣḥaf we ship prints 6,218 ayat while its counting system totals 6,214. Neither is a typo; they answer different questions.
What to do: treat the riwayah as a property of your loaded data, not a display toggle — model it as an entity, not a column (Engineering, rule 1.2). It changes actual letters and actual counts, so anything you cached under one riwayah is invalid under another, which is what a cache key naming everything (Engineering, rule 8.1) is for.
Idea 3 — Arabic text is not one character per letter
The text looks fine on screen while your indices point at the wrong place.
Two files containing the same ayah, rendering identically in your browser, can compare unequal, because one stores a shadda before its vowel and the other after, or one uses a precomposed آ where the other uses ا plus a separate mark. Between two of our own repositories, joining one dataset's words with a single space matches the other's text in only 319 of 6,236 ayahs.
We shipped this bug ourselves. Character offsets from one dataset applied to another dataset's text rendered 11 of 59 al-Fatiha annotations on the wrong letters, with nothing thrown.
What to do: never slice Arabic at an arbitrary index, since a cut falls on a grapheme cluster (Quranic text, rule 7.3)
and length is not letters (rule 2.5); never match on an Arabic string to join datasets, because the word key is surah:ayah:word (Engineering, rule 2.2);
and treat offsets as belonging to one specific text, which is offsets naming their text (Quranic text, rule 4.4).
Idea 4 — Prove you have the right text
A wrong-but-plausible text does not fail; it mislabels. The digest turns that into an error at load time, so every dataset we publish ships one and fails loudly (Quranic text, rule 8.4) on a mismatch.
What to do: check the digest when you load, and record which source and version you shipped — a manifest travels beside every file (Versioning and corrections, rule 1.2).
Joining datasets
You do not join our datasets on Arabic strings. You join them on a number: the word key (Engineering, rule 2.2).
Text, page coordinates and annotations all address the same word the same way, so highlighting, tapping and annotating line up without any of them agreeing on encoding.