Moroccan Darija NER and semiotic class tagging — rule-based NER engine that outperforms GPT-4o-mini on Darija text. Built in Rust for O(1) lookup, 750x faster than LLMs.
Project description
darija-router
Moroccan Darija NER and semiotic class tagging — rule-based, zero-training, O(1) lookup. Built in Rust, wrapped for Python.
63.0% F1 on Darija NER — outperforms GPT-4o-mini (57.0%) at 750x the speed.
Benchmark
Evaluated on 500 Moroccan Darija sentences (998 annotations, 12 entity types). All LLM baselines zero-shot via OpenRouter API.
| System | Precision | Recall | F1 | Speed |
|---|---|---|---|---|
| darija-router | 57.8% | 69.3% | 63.0% | 1.2s |
| GPT-4o-mini | 62.9% | 52.2% | 57.0% | ~15min |
| GPT-4o | 64.6% | 43.2% | 51.8% | ~20min |
| Qwen3.6-35B | 61.4% | 23.1% | 33.6% | ~45min |
| GPT-5-mini | 65.9% | 8.9% | 15.7% | ~10min |
darija-router runs on a single CPU core. No GPU. No API calls. No training data.
Ablation Study
| Configuration | P | R | F1 |
|---|---|---|---|
| Full system | 57.8% | 69.3% | 63.0% |
| − Lexicon lookup | 57.8% | 69.3% | 63.0% |
| − Morphological analysis | 57.8% | 69.3% | 63.0% |
| − NER triggers (PER/LOC/ORG) | 57.6% | 70.0% | 63.2% |
| − Temporal patterns | 62.0% | 59.7% | 60.9% |
| − All semantic tagging | 0.0% | 0.0% | 0.0% |
See eval/README.md for full methodology and per-label results.
Install
pip install darija-router
Requires Python ≥ 3.8. Wheels available for Linux, macOS, and Windows (x86_64 and ARM64).
Quick Start
import darija_router
# NER — extract named entities
result = darija_router.run_ner("شريت 3 كيلو ديال التفاح ب 20 درهم ليوم")
print(result["entities"])
# [{"text": "3 كيلو", "entity_type": "MEASURE", ...},
# {"text": "20 درهم", "entity_type": "MONEY", ...},
# {"text": "ليوم", "entity_type": "DATE", ...}]
# Filter to specific entity types
result = darija_router.run_ner(
"شريت 3 كيلو ب 20 درهم",
labels=["MONEY", "MEASURE"]
)
print(result["entities"])
# [{"text": "3 كيلو", "entity_type": "MEASURE", ...},
# {"text": "20 درهم", "entity_type": "MONEY", ...}]
# Code-switching detection
cs = darija_router.run_codeswitch("kifash nchri car b 5000 euro?")
print(cs["stats"]["darija"]) # 0.4
print(cs["stats"]["fr"]) # 0.2
# Full analysis
result = darija_router.analyze("خويا باغي نلقاك غدوة الساعة 3 صباح")
API Reference
run_ner(phrase, labels=None, config=None)
Named Entity Recognition. Returns span-based entities and per-token NER tags.
Parameters:
phrase(str): Input text (Arabic script or Arabizi)labels(list[str], optional): Filter output to only these entity types. Default: all types. Useful for evaluation when the gold schema excludes PERSON, LOC, etc.config(dict, optional): Pipeline configuration for ablation studies. Keys:disable_lexicon,disable_morphology,disable_semantic,disable_temporal,disable_ner_triggers. All default toFalse.
Returns:
{
"entities": [
{"text": "500 درهم", "entity_type": "MONEY", "start": 0, "end": 8,
"token_indices": [0, 1], "confidence": 0.95}
],
"tokens": [
{"token": "500", "ner_tag": "MONEY"},
{"token": "درهم", "ner_tag": "MONEY"}
]
}
analyze(phrase, mode=None, fields=None)
Tokenizes a phrase and classifies each token.
Parameters:
phrase(str): Input text in Arabic scriptmode(str, optional): Output preset —"full"(default),"compact","ner","codeswitch"fields(list[str], optional): Custom field selection — e.g.["token", "lang", "confidence"]
Returns: list[dict] with per-token results.
run_codeswitch(phrase)
Code-switching detection with language spans and statistics.
Returns:
{
"spans": [{"text": "خويا واش", "lang": "darija", "start": 0, "end": 9}],
"tokens": [{"token": "خويا", "lang": "darija", "latin": null, "confidence": 1.0}],
"stats": {"darija": 50.0, "fr": 25.0, "en": 0.0, "num": 12.5, "punct": 0.0, "other": 0.0, "unknown": 12.5}
}
lookup(word)
Single word lookup. Same format as one element from analyze().
normalize_arabic(text) / arabizi_to_arabic(text) / normalize(text)
Normalization functions. normalize() auto-detects script and applies the appropriate function.
tokenize(text)
Tokenize text into tokens with character offsets.
detect_script(text)
Detect script: "arabic", "latin", "mixed", or "unknown".
__version__
Package version string (e.g. "0.15.0").
Entity Types
| Type | Detection | Example |
|---|---|---|
CARDINAL |
Digits or Arabic number words | 50 عشرين مية |
ORDINAL |
Arabic ordinal words (standard + Darija) | تاني تالت اللول |
DECIMAL |
Decimal numbers | 3.5 ١٢.٥ |
MONEY |
Cardinal + currency word | 500 درهم |
MEASURE |
Cardinal + unit word | 2 كيلو 24 ساعة |
TIME |
Clock times, periods, durations | الساعة 3 صباح 8 د الصباح |
DATE |
Date words, years, day names, durations | غدوة 15 مارس 2026 |
PHONE |
Phone number patterns (Moroccan) | +212661234567 0661234567 |
EMAIL |
Email addresses | user@gmail.com |
URL |
URLs | http://google.com |
PERCENT |
Percentage suffixes | 20% بالمئة |
ADDRESS |
Address keywords + following word | زنقة الحسن |
PERSON |
Honorifics + name expansion | سيدي محمد لالة فاطمة |
LOC |
Location triggers + expansion | شارع محمد الخامس مدينة الدار البيضاء |
ORG |
Organization triggers + expansion | شركة مسير بنك الشعب |
FACILITY |
Facility triggers + expansion | سبيسان مسجد الكبير |
EVENT |
Event triggers + expansion | عيد الفطر عرس |
MISC |
Nationalities, languages, religions | المغربي الفرنساوي الإسلام |
FRACTION |
Arabic fraction words | نص ربع تلت |
Semantic Tags
Semantic tags are internal per-token labels that feed into the NER engine. They correspond to entity types above but may include intermediate labels like PUNCT.
| Tag | Detection | Example |
|---|---|---|
PUNCT |
Punctuation characters | ؟ ، . |
CARDINAL |
Digits or Arabic number words | 50 عشرين مية |
ORDINAL |
Arabic ordinal words | تاني تالت اللول |
MONEY |
Cardinal + currency word | 50 درهم → both tokens |
MEASURE |
Cardinal + unit word | 2 كيلو → both tokens |
TIME |
Time patterns | الساعة 3 صباح |
DATE |
Date patterns | غدوة 15 مارس |
FRACTION |
Fraction words | نص ربع |
PERCENT |
Percentage suffix | 20% |
PHONE |
Phone patterns | +212... |
EMAIL |
Email patterns | user@... |
URL |
URL patterns | http://... |
ADDRESS |
Address keywords | زنقة شارع |
Output Modes
| Mode | Description |
|---|---|
"full" (default) |
All fields: token, index, lang, entries, stem, semantic, confidence |
"compact" |
token + lang only (no entries/stem) |
"ner" |
NER preset: entities + tokens with ner_tag |
"codeswitch" |
Code-switching preset: spans + tokens + stats |
Confidence Scoring
| Match Type | Score |
|---|---|
| Exact match | 1.0 |
| Normalized (ة→ه) | 0.95 |
| Single prefix/suffix strip | 0.85 |
| Double strip (prefix + suffix) | 0.70 |
| Semantic tag only (no lexicon match) | 0.60 |
| Unknown | 0.0 |
Pipeline Architecture
darija-router processes text through a six-phase pipeline:
- Script Detection — classify each token as Arabic, Latin, Mixed, or Numeric
- Normalization — Arabizi (Latin script) → Arabic script using dictionary + heuristics (427 exceptions)
- Morphological Analysis — strip Arabic prefixes (7) and suffixes (5) withت→ه normalization
- Lexicon Lookup — O(1) HashMap lookup against 2,187 embedded entries, with morphological fallback
- Semantic Tagging — context-aware rule engine detecting 14+ categories with multi-token grouping
- NER Extraction — trigger-based detection for PERSON, LOC, ORG, FACILITY, EVENT, MISC with expansion rules
All lexicons compile into the binary via include_str!(). Zero runtime file I/O. Zero startup cost. 3.2 MB binary.
Temporal Expression Recognition
darija-router recognizes 12 distinct Darija temporal patterns:
| Pattern | Example | Structure |
|---|---|---|
| Clock time | الساعة 8 |
article + number |
| Prepositional | 8 د الصباح |
number + preposition + period |
| Contracted | 12 دالليل |
number + attached preposition |
| Duration hours | 24 ساعة |
number + hour unit |
| Duration minutes | 15 دقيقة |
number + minute unit |
| Fraction hour | نص ساعة |
fraction + unit |
| Clock with fraction | 6 ونص |
number + connector + fraction |
| Midnight | منتصف الليل |
fixed phrase |
| Noon | منتصف النهار |
fixed phrase |
| Period only | 8 مساء |
number + period word |
| Definite hour | السبعة |
article + number word |
| HH:MM | 14:30 |
direct time notation |
This specialized handling improved TIME F1 from 0% to 38.1%.
Build from Source
git clone https://github.com/LaamiriOuail/darija-router.git
cd darija-router
pip install maturin
maturin develop --release
Testing
pip install pytest
pytest tests/ -v
695+ tests covering tokenization, normalization, morphology, semantic tagging, NER, code-switching, and span positions.
Citation
If you use darija-router in your research, please cite:
@software{darija_router_2026,
author = {Laamiri, Ouail and Berrada, Ismail},
title = {darija-router: Rule-Based NER for Moroccan Darija},
year = {2026},
url = {https://github.com/LaamiriOuail/darija-router},
note = {63.0\% F1 on Darija NER, outperforming GPT-4o-mini at 750x speed}
}
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file darija_router-0.15.0-cp312-cp312-win_amd64.whl.
File metadata
- Download URL: darija_router-0.15.0-cp312-cp312-win_amd64.whl
- Upload date:
- Size: 289.4 kB
- Tags: CPython 3.12, Windows x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: maturin/1.13.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5bf729beb10eba346480a6e229b2a331cd5afc924403d64f8cbeac058da14533
|
|
| MD5 |
a540d4f8baa7e09e7cee9cfa801a0de3
|
|
| BLAKE2b-256 |
e30edc6fa38a705de051ecc539d03291a295b0b794628c1f901a46bf5dfd046b
|
File details
Details for the file darija_router-0.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: darija_router-0.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 367.3 kB
- Tags: CPython 3.12, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: maturin/1.13.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d5036a53a8707c99ab020efcdc85166b848c1660e86b247e7307604cf40774d9
|
|
| MD5 |
5a8c896a51159562dd4d1300e6610524
|
|
| BLAKE2b-256 |
258aaeb3389061e531dd4cc5c4de75c2767f1bcf788b9f8a732e97bd72867ab6
|
File details
Details for the file darija_router-0.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.
File metadata
- Download URL: darija_router-0.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
- Upload date:
- Size: 350.3 kB
- Tags: CPython 3.12, manylinux: glibc 2.17+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: maturin/1.13.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
76530d0b569088f631396829c3262b6c8f576cad3400450e2c6718585e3d9aa4
|
|
| MD5 |
40dede94d95f09e645b5f0564db7c98b
|
|
| BLAKE2b-256 |
0ffeebf39b6e55b88ed040ee5904de0b465b2f025e9b96df73b7dbde633b8a65
|
File details
Details for the file darija_router-0.15.0-cp312-cp312-macosx_11_0_arm64.whl.
File metadata
- Download URL: darija_router-0.15.0-cp312-cp312-macosx_11_0_arm64.whl
- Upload date:
- Size: 341.0 kB
- Tags: CPython 3.12, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: maturin/1.13.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
226fb270f5113a15418cf8bb10ad02f3fd6839c9201c2d7cebb92a49bf2ae68f
|
|
| MD5 |
60dfa83d2fd303af00e077d0e71a9d63
|
|
| BLAKE2b-256 |
a4c871577ab5984113453b4826cc3e498aa9ba3f42c857ac109020b728161402
|
File details
Details for the file darija_router-0.15.0-cp312-cp312-macosx_10_12_x86_64.whl.
File metadata
- Download URL: darija_router-0.15.0-cp312-cp312-macosx_10_12_x86_64.whl
- Upload date:
- Size: 356.7 kB
- Tags: CPython 3.12, macOS 10.12+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: maturin/1.13.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4733bd82da9acc8f13a13f1031d4319c7383af11a871c48d59b44f3a17cbe254
|
|
| MD5 |
d72522efdb7c46460dc38bae639b0441
|
|
| BLAKE2b-256 |
831bf203b7d794dbed70a52d7b1d8eac176626a59619971bf8a027fdf9927e55
|