Search the text

A user types الرحمن. The muṣḥaf prints ٱلرَّحۡمَٰنِ. Neither === nor includes() will ever connect the two. This page is what to index so that they connect, and where the packages disagree about how.

Platforms
Anywhere the text loads
Time
About 10 minutes

Why the naive version finds nothing

The printed text carries vowel marks between the letters, a special form of the initial alif (ٱ, hamzat al-waṣl), a small “dagger” alif above the line where modern spelling writes a full alif, and pause signs after some words. A user types none of those. So the printed word and the typed word are different strings of different lengths:

"ٱلرَّحۡمَٰنِ" === "الرحمن";            // false
"ٱلرَّحۡمَٰنِ".includes("الرحمن");      // false

The answer is to match on a folded form, the bare letters with the marks stripped and the letter variants unified, and every package here ships one. None of them ships the same one; see the end of this page.

Search with Quran Text

  1. Load an edition

    Search runs over one edition at a time, because the spelling differs between editions. Start with Ḥafṣ.

    import { Mushaf, fold } from "quran-text";
    
    const m = await Mushaf.hafs();
    
  2. See what the fold does

    fold() is exported so you can inspect it. It drops the marks, turns every alif variant into a plain alif, every yāʾ variant into a plain yāʾ, and turns the dagger alif into a full alif.

    fold("مَٰلِكِ يَوۡمِ ٱلدِّينِ");   // "مالك يوم الدين"
    fold("ٱلرَّحۡمَٰنِ");             // "الرحمان"   ← full alif where the print has a dagger alif
    
  3. Call search, and keep the positions

    m.search(text) folds the query the same way and returns every place its words occur in sequence, as whole words. Each result is a Span, a run of positions into the edition’s words array, and everything you want to show hangs off it.

    /** Find every ayah in which the typed words occur in sequence. */
    function findAyat(query) {
      return m.search(query).map((span) => {
        const ayah = span.firstAyah;
        return {
          key: ayah.key,                 // "1:4"
          page: ayah.page.number,        // 1
          match: [span.start, span.end], // positions into m.words — highlight these
          text: ayah.text,               // the printed words, unmarked
        };
      });
    }
    
    findAyat("مالك يوم الدين");
    // [{ key: "1:4", page: 1, match: [10, 13], text: "مَٰلِكِ يَوۡمِ ٱلدِّينِ" }]
    
    findAyat("الحمد لله رب العالمين").map((r) => r.key);
    // ["1:2", "10:10", "39:75", "40:65"]
    

    Highlight by match, the positions, never by finding the string again in the rendered text. The rendered text has marks in it; the positions do not care.

  4. Look a single word up across all seven editions

    WordIndex is the 50 MB file for servers and build steps, not phones. Its search takes one folded word and returns every numbered word that folds to it, with each edition’s spelling.

    import { WordIndex } from "quran-text";
    
    const idx = await WordIndex.load("data/word-index.json");
    idx.search("مالك").map((w) => [w.number, w.forms.hafs, w.forms.warsh]);
    // [[11, "مَٰلِكِ", "مَلِكِ"], [6566, "مَٰلِكَ", "مَٰلِكَ"]]
    
    idx.find(2, 255, 3).key;   // "2:الاه#3" — content-derived, stable across rebuilds
    

    Match on fold() or on the index’s plain field. Never match on the printed spelling: it differs between editions, and between releases of the same edition.

Search with Quran Engine or Quran SVG Elements

On a rendered page the search index is already there. The Elements sidecar and the engine’s NNN.words.json give every word a search form, and the engine searches it for you, with a loose retry when the strict pass finds nothing. On page 001:

page.attachWords(wordsJsonText);   // 29 — without this, search runs over the stripped print instead
page.hasForm("search");            // true

page.search("الرحمن");
// [{ word: 2, wordKey: "1:1:3", text: "ٱلرَّحْمَٰنِ", loose: false }, { word: 8, wordKey: "1:3:1", … }]
page.search("الرحمان");   // the same two words, loose: true
page.search("ملك");       // [{ word: 10, wordKey: "1:4:1", text: "مَٰلِكِ", loose: true }]

wordKey is the surah:ayah:word address every block shares, so a hit here is already a highlight target, a Quran Text lookup and a bookmark. The Quran Engine reference covers mode (includes, exact, prefix) and what happens without the sidecar.

The two folds are not the same fold

The same three queries, against the same words, in two of our own packages:

TypedQuran Text m.search(), whole muṣḥafQuran Engine page.search(), page 001
الرحمن — the spelling everyone types0 results2 results, strict
الرحمان45 results2 results, via the loose retry
ملك31 results — the word ملك, not مالك1 result — مَٰلِكِ, via the loose retry

The cause is one decision made two ways. Quran Text’s fold() turns the dagger alif into a full alif, so the printed al-Raḥmān becomes الرحمان; the Elements and engine search form drops it, so the same word becomes الرحمن. A user’s الرحمن matches one and not the other.

Zero results is not an error

m.search("الرحمن") returns an empty array, and an empty array is a valid result: the user concludes the app cannot find the most common word in the book. Quran Text fixed its fold at 2aac0b1 (11 September 2026) and the engine adopted the same spec the same day, so at those commits both spellings find 45 ayahs; the copy this site is pinned to predates that. Whatever version you ship, fold the query with the function the index was built with and show the result count so an empty result is visible.

Three rules fall out of this:

What this is not for

You wantUse
To search by root, lemma or meaningAnother dataset; none of these blocks carries morphology
To search a translationYour translation’s own text; join the hit to the Qur’an by surah:ayah
To find the page an ayah sits onm.ayah(s, a).page.number, or the engine atlas — no search needed
Try it live

The engine running in your browser on the real wasm. Search runs inside the engine against the sidecar’s search form.