Map ayah references between counts
The verse the Ḥafṣ muṣḥaf numbers 7:206 is numbered 7:205 in two of the six counting systems. This guide translates a reference from one system into another by counting system and by transmitter, with no text loaded, returning an array where the dataset returns one and never doing arithmetic.
The example, from the data
Surah 7, al-Aʿrāf, has 206 ayat in the Kufan count. Two systems count 205, because a boundary Kufan counts at ayah 29 they do not. So the last verse of the surah moves:
| Counting system | Ayat in surah 7 | Kufan 7:206 is |
|---|---|---|
| First Madinan madani-first | 206 | 7:206 |
| Last Madinan madani-last | 206 | 7:206 |
| Makkan makki | 206 | 7:206 |
| Basran basri | 205 | 7:205 |
| Damascene dimashqi | 205 | 7:205 |
| Kufanreference kufi | 206 | 7:206 |
Computed at build time from public/demo/qiraat.json — the disputed-boundary layer of Qiraat Ayah Map — using the same rebuild the live demo runs. Surah 7, Al-A'raf, has 206 ayat in the Kufan count.
The generated mapping files say the same: kufi-to-basri.json gives Kufan 7:206 { "target_ayah": 205, "status": "mapped" }, and
kufi-to-madani-last.json gives { "target_ayah": 206, "status": "mapped" }.
By the end you have a function that takes (surah, ayah) in Kufan numbering and a transmitter, and
returns the ayah number or numbers in that transmitter's system, plus the reverse.
Get the mapping files
The mapping, count and transmitter files are generated artefacts, written to
dist/by the repository'sgeneratescript. At the commit this guide was read against they are not in the git tree, not a release asset, and not on npm (package.jsonis markedprivate). So:git clone https://github.com/quran-ws/qiraat-ayah-map.git cd qiraat-ayah-map && npm run generate # writes dist/Then copy
dist/into your project and serve it as static JSON. Everything below is a lookup in those files;loadis whatever you use to read JSON.File Holds dist/rawis/<rawi>.jsonwhich counting system a transmitter follows, and which mapping file to load dist/mappings/by-counting-system/kufi-to-<system>.jsonKufan number → that system's number dist/mappings/by-counting-system/<system>-to-kufi.jsonthe reverse dist/surah-counts/<system>.jsonayat per surah, and the total, in that system Five non-Kufan systems, so ten mapping files. The
by-rawi/directory holds the same tables named by transmitter:hafs-to-warsh.jsoniskufi-to-madani-last.jsonwith a_rawifield.Start from the transmitter the user chose
Your UI offers readings, not counting madhhabs. The transmitter file is the join, and it is small:
const rawi = await load("dist/rawis/warsh.json"); // { _rawi: "warsh", _qiraa: "nafi", _counting_system: "madani-last", _mushaf_id: 4, // _mapping_file: "mappings/by-counting-system/kufi-to-madani-last.json" } const hafs = await load("dist/rawis/hafs.json"); // { …, _counting_system: "kufi", _mapping_file: null } ← Kufan already; nothing to translateA
null_mapping_filemeans the transmitter is Kufan and the numbers are already Ḥafṣ's. Eight of the twenty transmitter files are like that.Load both directions for that system
const system = rawi._counting_system; // "madani-last" const forward = rawi._mapping_file ? await load(`dist/${rawi._mapping_file}`) : null; const reverse = rawi._mapping_file ? await load(`dist/mappings/by-counting-system/${system}-to-kufi.json`) : null;Entries are keyed by surah number then ayah number, both as strings:
forward.surahs["7"].ayahs["206"].Convert, and branch on status every time
A forward entry has one of three statuses, and the answer depends on which:
statusMeaning Extra field mappedone Kufan ayah, one target ayah — mergedthis ayah joins the following one in the target merges_with_next: truesplitthis ayah becomes two or more in the target splits_into: number[]/** Kufan (surah, ayah) → the number(s) it occupies in the transmitter's system. Always an array. */ function toSystem(forward, surah, ayah) { if (!forward) return [ayah]; const entry = forward.surahs[String(surah)].ayahs[String(ayah)]; return entry.status === "split" ? entry.splits_into : [entry.target_ayah]; } const basri = await load("dist/mappings/by-counting-system/kufi-to-basri.json"); const madaniLast = await load("dist/mappings/by-counting-system/kufi-to-madani-last.json"); toSystem(basri, 7, 206); // [205] toSystem(madaniLast, 7, 206); // [206] toSystem(basri, 7, 29); // [28, 29] — one Kufan ayah is two Basran ayat toSystem(madaniLast, 7, 1); // [1] — status "merged": runs into 7:2 toSystem(madaniLast, 1, 7); // [6, 7] toSystem(madaniLast, 2, 255); // [253, 254]target_ayah alone silently loses ayatIn
kufi-to-madani-last.json, Kufan 1:1 and 1:2 both carrytarget_ayah: 1, because Last Madinan counts them as one ayah. Code that readstarget_ayahand ignoresstatusproduces a plausible number every time: it renders two verses under one number and drops the second half of every split. Return the array and let the caller decide how to show a range.merges_with_nextis not a fourth status. It can appear besidesplit: Kufan 7:29 inkufi-to-basri.jsonis{ "target_ayah": 28, "status": "split", "splits_into": [28, 29], "merges_with_next": true }: it splits and its tail runs into 7:30. Test the field, not the status, if merging is what you care about.Go back to Kufan for storage
The reverse file has two shapes:
mapped, withhafs_ayah; andcovers_multiple, where one ayah in the system spans several Kufan ayat andhafs_ayahsholds them all./** (surah, ayah) in the transmitter's system → the Kufan ayah number(s) that cover it. */ function toKufan(reverse, surah, ayah) { if (!reverse) return [ayah]; const entry = reverse.surahs[String(surah)].ayahs[String(ayah)]; return entry.status === "covers_multiple" ? entry.hafs_ayahs : [entry.hafs_ayah]; } const basriToKufi = await load("dist/mappings/by-counting-system/basri-to-kufi.json"); toKufan(basriToKufi, 7, 205); // [206] toKufan(basriToKufi, 7, 1); // [1, 2] toKufan(basriToKufi, 7, 29); // [29, 30]Reverse then forward is not the identityKufan 1:7 splits into Last Madinan 1:6 and 1:7. The reverse file maps both of those back to Kufan 7 as
mapped, which is correct, but it means a round trip cannot tell you which half you started from. Store the Kufan reference, translate outward for display, and never use the round trip to "normalise" a number.Show the count, from the count file
The picker's range in the user's reading comes from
surah-counts, never from the Kufan count plus or minus something:const counts = await load(`dist/surah-counts/${system}.json`); counts._total_ayahs; // 6214 for madani-last, 6204 for basri counts.surahs["7"]; // 206 for madani-last, 205 for basri counts.surahs["2"]; // 285 for madani-last, 287 for basriThere is no delta to addBasran totals 32 fewer ayat than Kufan, and its al-Baqarah has one more. Differences run in both directions and cancel. No offset computed from totals, and none carried across a surah, gives the right number. Read the count, or map the reference.
Put it together
async function forTransmitter(rawiKey) { const rawi = await load(`dist/rawis/${rawiKey}.json`); const system = rawi._counting_system; const forward = rawi._mapping_file ? await load(`dist/${rawi._mapping_file}`) : null; const reverse = forward ? await load(`dist/mappings/by-counting-system/${system}-to-kufi.json`) : null; const counts = await load(`dist/surah-counts/${system}.json`); return { system, display: (surah, ayah) => toSystem(forward, surah, ayah), // Kufan in, array out store: (surah, ayah) => toKufan(reverse, surah, ayah), // reading's number in, Kufan out ayahsIn: (surah) => counts.surahs[String(surah)], }; } const warsh = await forTransmitter("warsh"); warsh.display(7, 206); // [206] warsh.display(2, 255); // [253, 254] warsh.ayahsIn(2); // 285Persist
{ surah: 7, ayah: 206, counting: "kufi" }. Displaywarsh.display(7, 206). Nothing in between does arithmetic.
Pick a reference and watch it resolve to a different number in each of the six systems, from this dataset.
Two datasets, one contested transmitter
Qiraat Ayah Map assigns al-Sūsī and al-Dūrī, the transmitters of Abū ʿAmr, to Basran, so
dist/rawis/susi.json sends 7:206 to 7:205. Quran Text measures the printed al-Sūsī muṣḥaf
closest to First Madinan, so hafs.ayah(7, 206).to(susi) returns 7:206, relation "same", and
the muṣḥaf it ships prints 6,218 ayat against Basran’s 6,204. Both are defensible about
different questions: the scholarly assignment of a qāriʾ, and what a particular publisher set in
type. The contradiction is recorded, unresolved, and needs a scholar; until then, say which of the
two your product follows.
What to put in front of the reader
Show system or reciter names, not the internal identifiers: "Warsh" and "Last Madinan", never
madani-last — the UI may translate what the identifier may not (Naming, rule 5.4). Keep documentation state, evidence tiers and maintenance tables out of the reading
surface: they belong on a detail view a reader opens for itself, which is how the
reference itself arranges them.
And the shortcut worth knowing: the eight Kufan transmitters share Ḥafṣ numbering exactly, so for
those there is nothing to convert. rawis/<rawi>.json tells you with a null _mapping_file.
Related guides
| You want | Use instead |
|---|---|
| The text of the translated ayah | Quran Text — this dataset carries no text |
| Translate between two loaded printed editions | ayah.to(other) in Quran Text — see Support multiple riwayat |
| Basran to Makkan directly | Convert through Kufan in two steps; there is no basri-to-makki.json |
How this data is made
The source layer is a list of every place the traditions disagree about a boundary, each disputed point recorded once, anchored by the word it turns on, with the systems that count it and the classical works it is cited from. Every mapping file and every count is derived from that layer, and rebuilding all six numberings from it reproduces the six published totals: 6,236, 6,226, 6,219, 6,214, 6,214 and 6,204. The table at the top of this page is that same rebuild, run over one surah at build time.