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.
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
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();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 alifCall 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 aSpan, a run of positions into the edition’swordsarray, 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.Look a single word up across all seven editions
WordIndexis the 50 MB file for servers and build steps, not phones. Itssearchtakes 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 rebuildsMatch on
fold()or on the index’splainfield. 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:
| Typed | Quran Text m.search(), whole muṣḥaf | Quran Engine page.search(), page 001 |
|---|---|---|
الرحمن — the spelling everyone types | 0 results | 2 results, strict |
الرحمان | 45 results | 2 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.
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:
- Never build your own normaliser, because the search key is derived, not typed (Quranic text, rule 6.3). Use the one the dataset exports, so the query is folded exactly as the index was — use the dataset's fold (Engineering, rule 7.1).
- Never search one dataset’s index with another dataset’s fold. They are close, and close is what produces confident zeros.
- Whole words, in sequence. Quran Text’s search has no prefix or substring mode:
ملكfinds the 31 places the word malik is printed and does not find mālik. The engine hasmode: "prefix".
What this is not for
| You want | Use |
|---|---|
| To search by root, lemma or meaning | Another dataset; none of these blocks carries morphology |
| To search a translation | Your translation’s own text; join the hit to the Qur’an by surah:ayah |
| To find the page an ayah sits on | m.ayah(s, a).page.number, or the engine atlas — no search needed |
The engine running in your browser on the real wasm. Search runs inside the engine against the sidecar’s
search form.