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.

Platforms
Any — JSON files, no runtime
Time
About 12 minutes

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 systemAyat in surah 7Kufan 7:206 is
First Madinan
madani-first
2067:206
Last Madinan
madani-last
2067:206
Makkan
makki
2067:206
Basran
basri
2057:205
Damascene
dimashqi
2057:205
Kufanreference
kufi
2067: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.

  1. Get the mapping files

    The mapping, count and transmitter files are generated artefacts, written to dist/ by the repository's generate script. At the commit this guide was read against they are not in the git tree, not a release asset, and not on npm (package.json is marked private). 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; load is whatever you use to read JSON.

    FileHolds
    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.json is kufi-to-madani-last.json with a _rawi field.

  2. 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 translate
    

    A null _mapping_file means the transmitter is Kufan and the numbers are already Ḥafṣ's. Eight of the twenty transmitter files are like that.

  3. 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"].

  4. Convert, and branch on status every time

    A forward entry has one of three statuses, and the answer depends on which:

    statusMeaningExtra field
    mappedone Kufan ayah, one target ayah
    mergedthis ayah joins the following one in the targetmerges_with_next: true
    splitthis ayah becomes two or more in the targetsplits_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 ayat

    In kufi-to-madani-last.json, Kufan 1:1 and 1:2 both carry target_ayah: 1, because Last Madinan counts them as one ayah. Code that reads target_ayah and ignores status produces 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_next is not a fourth status. It can appear beside split: Kufan 7:29 in kufi-to-basri.json is { "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.

  5. Go back to Kufan for storage

    The reverse file has two shapes: mapped, with hafs_ayah; and covers_multiple, where one ayah in the system spans several Kufan ayat and hafs_ayahs holds 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 identity

    Kufan 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.

  6. 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 basri
    
    There is no delta to add

    Basran 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.

  7. 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);        // 285
    

    Persist { surah: 7, ayah: 206, counting: "kufi" }. Display warsh.display(7, 206). Nothing in between does arithmetic.

Try it live

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

Careful

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-lastthe 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.

You wantUse instead
The text of the translated ayahQuran Text — this dataset carries no text
Translate between two loaded printed editionsayah.to(other) in Quran Text — see Support multiple riwayat
Basran to Makkan directlyConvert 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.