Search that still works when nobody types tone marks

    Restore the marks your documents lost, then match on an accent-folded key — so a user typing "oko" finds "ọkọ", and still sees it written properly.

    Media & publishingMarketplacesArchivesGovernment portals
    ~0.02
    MEDIAN CER ON RESTORATION
    10
    LANGUAGES RESTORED
    0
    MODELS ON THE QUERY PATH
    / The problem

    Why this breaks today

    A user searches "oko" and gets nothing, because the article says "ọkọ". A marketplace listing typed with tone marks never surfaces for the buyer who typed without them. Neither side did anything wrong — the index and the query are simply in different orthographies.

    Accent-folding both sides fixes the matching in one line of Unicode, and plenty of teams stop there. But it does nothing for the other half of the problem: a corpus that was scraped, OCR'd, or user-typed without marks is still broken everywhere it is displayed, and folding just hides that.

    So do both, and keep them separate: restore the marks so the text reads correctly, and fold a copy so it matches however anyone types.

    / How it works

    The pipeline, step by step

    01

    Settle the language before you restore anything

    For a mixed corpus of correctly written documents, lid-neural-25.1 labels them at 99.9%. For text that already lost its marks, set the language per source instead — language ID reads those marks too, so it is least reliable on exactly the text you are about to fix.

    lid-neural-25.1
    02

    Restore only the documents that need it

    Run diacnet-1.0 over the text that is missing its marks — scraped pages, OCR output, user submissions. Skip anything already written correctly: the model maps stripped text to marked text, so running it on correct text double-applies the marks and corrupts them.

    diacnet-1.0
    03

    Index an accent-folded copy as the match key

    Fold the restored text down to unmarked characters and store that as the search field, keeping the restored text for display. This is pure Unicode — NFD, drop the combining marks — so no model sits on your indexing path.

    04

    Fold the query the same way

    One function, both sides. A query typed with marks and one typed without now collapse to the same key, and no model runs on the query path at all — so search latency is untouched.

    / The code

    Running in about ten lines

    The Olaverse SDK wraps the models with sane defaults. If you would rather not add a dependency, the second tab is the same pipeline in plain transformers.

    $pip install olaverse[deeplearning]
    normalise.py
    import unicodedata
    from olaverse.nlp import Diacritizer
    
    # Your corpus language is something you know — don't infer it from stripped text
    d = Diacritizer(model="diacnet-1.0", lang="yo")
    
    def fold(s: str) -> str:
        """Accent-fold to a match key. Pure Unicode, no model — ọ/ẹ/ṣ decompose to
           o/e/s plus a combining mark, so NFD and dropping Mn covers Yoruba."""
        return "".join(c for c in unicodedata.normalize("NFD", s)
                       if unicodedata.category(c) != "Mn").lower()
    
    def index_document(doc):
        # Restore only what's missing its marks; never re-run it on correct text
        text = doc["text"] if doc["has_diacritics"] else d.restore(doc["text"])
        return {**doc, "display_text": text, "match_text": fold(text)}
    
    corpus = [
        {"id": 1, "text": "Ṣé ẹranko náà sì gbọ́ ọ?", "has_diacritics": True},    # clean CMS copy
        {"id": 2, "text": "se eranko naa si gbo o?",  "has_diacritics": False},   # scraped / OCR
    ]
    indexed = [index_document(c) for c in corpus]
    # both now share one match_text: 'se eranko naa si gbo o?'
    # and doc 2 displays as 'ṣé ẹranko náà sì gbọ́ ọ?' instead of the stripped original
    
    # The query path is folding only — no model, no added latency
    key = fold("SE ERANKO NAA")
    [c["id"] for c in indexed if key in c["match_text"]]     # → [1, 2]

    Language coverage

    / 10 LANGUAGES RESTORED

    Every language the models in this pipeline were trained on, with the codes you pass and the codes you get back.

    Yorùbá
    yo · yor
    Vietnamese
    vi · vie
    Igbo
    ig · ibo
    Hausa
    ha · hau
    Polish
    pl · pol
    Turkish
    tr · tur
    Portuguese
    pt · por
    Spanish
    es · spa
    French
    fr · fra
    Italian
    it · ita

    Restoration covers these ten. Anything else still benefits from the folding half of the pipeline — it just gets indexed as typed, with no restoration pass.

    / This is you if
    • You run search over Yorùbá, Igbo, Hausa, Vietnamese, or another diacritic-heavy language.
    • Your analytics show a rate of zero-result queries you cannot explain.
    • Your content is correctly written but your users type on a phone keyboard.
    / What it can't do
    • Never run the restorer over text that already has its marks. It is trained to map stripped text to marked text, so on correct input it double-applies: "Ṣé ẹranko náà" comes back as "Ṣẹ́ ẹranko náàá". Gate it on a flag, as the snippet does.
    • Do not pick the language by running language ID over stripped text. LID reads the diacritics too: in our own spot-check on six Yorùbá sentences it scored 6/6 with the marks intact but 4/6 (short-text checkpoint) and 0/6 (long-form) once they were removed — and the misses were confident, so a score threshold will not catch them.
    • diacnet-1.0 covers ten languages; anything outside that set should be folded and indexed as typed.
    • Yorùbá restoration is strong but not perfect (29.1% WER), so the displayed text will carry a residual tail of errors. Folding means those errors cost you readability, not recall.
    • Folding is lexical matching. It is not a retrieval model; pair it with your existing embedding or BM25 stack.

    Every figure on this page comes from the published model cards, and every limitation is one the cards state themselves. Download the weights and check us.

    Want this on your data?

    The weights are open, so you can build it yourself this afternoon. If you would rather we tuned it to your domain, evaluated it properly, and handed it over, tell us what you are working on.