diff --git a/BEDIENUNGSANLEITUNG.md b/BEDIENUNGSANLEITUNG.md
new file mode 100644
index 0000000..04809c5
--- /dev/null
+++ b/BEDIENUNGSANLEITUNG.md
@@ -0,0 +1,746 @@
+# mdgrep – Bedienungsanleitung
+
+**mdgrep** ist eine Volltextsuche für Markdown-Dateien und beliebige andere Texte. Sie versteht
+Wortbestandteile, Phrasen, reguläre Ausdrücke und logische Verknüpfungen mit Klammern – und lässt
+sich auf vier Arten nutzen:
+
+| Baustein | Wofür |
+|---|---|
+| **Kommandozeile** `mdgrep` | schnell im Terminal suchen, wie `grep` – aber blockweise und mit UND/ODER/NICHT |
+| **Python-Bibliothek** | Suche in eigene Programme einbauen, auch für Nicht-Markdown-Daten |
+| **Web-API** (Flask) | JSON-Suchschnittstelle für Webseiten und andere Dienste |
+| **Such-Widget** (JavaScript) | fertige Suchmaske mit Live-Ergebnissen für jede Webseite |
+
+Version: 0.1.1 · Lizenz: MIT · Repository: https://kitux.de/forgejo/dschlueter/mdgrep
+
+---
+
+## Inhaltsverzeichnis
+
+1. [Installation](#1-installation)
+2. [Suchsyntax](#2-suchsyntax)
+3. [Kommandozeile](#3-kommandozeile)
+4. [Python-Bibliothek](#4-python-bibliothek)
+5. [Web-API](#5-web-api)
+6. [Such-Widget](#6-such-widget)
+7. [Betrieb auf einem Server](#7-betrieb-auf-einem-server)
+8. [Rezepte](#8-rezepte)
+9. [Fehlermeldungen](#9-fehlermeldungen)
+10. [Fehlersuche](#10-fehlersuche)
+11. [Entwicklung & neue Versionen](#11-entwicklung--neue-versionen)
+
+---
+
+## 1. Installation
+
+Voraussetzung: Python 3.10 oder neuer, `git` (für die Installation aus dem Repository).
+
+```bash
+# nur Bibliothek + Kommandozeile
+pip install "mdgrep @ git+https://kitux.de/forgejo/dschlueter/mdgrep.git@v0.1.1"
+
+# zusätzlich Web-API (installiert Flask)
+pip install "mdgrep[web] @ git+https://kitux.de/forgejo/dschlueter/mdgrep.git@v0.1.1"
+```
+
+In einer `requirements.txt`:
+
+```
+mdgrep[web] @ git+https://kitux.de/forgejo/dschlueter/mdgrep.git@v0.1.1
+```
+
+Prüfen:
+
+```bash
+mdgrep --version # mdgrep 0.1.1
+python -c "import mdgrep; print(mdgrep.__version__)"
+```
+
+> **Hinweis:** mdgrep ist nicht auf PyPI veröffentlicht. Immer mit Git-URL und Versions-Tag
+> installieren – ein bloßes `pip install mdgrep` findet das Paket nicht (oder, falls jemand den
+> Namen dort später belegt, ein fremdes).
+
+---
+
+## 2. Suchsyntax
+
+Groß- und Kleinschreibung spielen **nie** eine Rolle (auch nicht bei Umlauten).
+
+| Eingabe | Bedeutung | Beispiel findet … |
+|---|---|---|
+| `rust` | Wortbestandteil | „Rust", „Rustacean", „trusted" |
+| `"open source"` | genaue Phrase; beliebiger Leerraum (auch Zeilenumbruch) zwischen den Wörtern | „Open Source", „open
source" |
+| `/llms?\b/` | regulärer Ausdruck | „LLM", „LLMs" |
+| `a b` · `a AND b` · `a UND b` · `a & b` · `a && b` | beide Begriffe | |
+| `a OR b` · `a ODER b` · `a \| b` · `a \|\| b` | mindestens einer | |
+| `NOT a` · `NICHT a` · `-a` · `!a` | Begriff darf nicht vorkommen | |
+| `( … )` | Klammern | |
+
+**Vorrang:** NICHT bindet stärker als UND, UND stärker als ODER.
+`a OR b AND c` bedeutet also `a OR (b AND c)`. Im Zweifel Klammern setzen.
+
+**Hinweise:**
+
+- Operatoren werden nur in **Großbuchstaben** erkannt: `and` ist ein normales Suchwort, `AND` der Operator.
+- Wortbestandteile treffen auch mitten im Wort: `go` findet „**Go**ddard". Für **ganze Wörter**
+ einen regulären Ausdruck mit Wortgrenzen verwenden: `/\bgo\b/`.
+- Im regulären Ausdruck einen Schrägstrich als `\/` schreiben: `/tcp\/ip/`.
+- In Phrasen ein Anführungszeichen als `\"` schreiben.
+- Wörter mit Sonderzeichen funktionieren direkt: `AT&T`, `TCP/IP`, `e-mail`
+ (ein `-` oder `&` *innerhalb* eines Wortes ist kein Operator).
+
+**Beispiele:**
+
+```text
+(rust ODER go) UND NICHT crypto
+"machine learning" -/pytorch|tensorflow/
+llm (datenschutz OR privacy)
+/\bgpt-\d+\b/ NICHT "open source"
+```
+
+**Grenzen (Standard):** höchstens 500 Zeichen und 50 Suchbegriffe pro Anfrage
+(die Web-API begrenzt zusätzlich auf 300 Zeichen).
+
+---
+
+## 3. Kommandozeile
+
+```bash
+mdgrep SUCHANFRAGE [PFAD ...] [Optionen]
+```
+
+Ohne Pfad wird das aktuelle Verzeichnis durchsucht. Verzeichnisse werden rekursiv nach `*.md`
+durchsucht. Die Suchanfrage immer in **einfache Anführungszeichen** setzen, damit die Shell
+Klammern, `!`, `|` und `&` nicht selbst auswertet.
+
+### 3.1 Beispiele
+
+```bash
+mdgrep '(rust OR go) AND NOT crypto' notizen/ # alle *.md unter notizen/
+mdgrep '/\bllms?\b/' docs/*.md --split list # ein Treffer pro Listenpunkt
+mdgrep 'todo' . -l # nur Dateinamen
+mdgrep 'release' . -c # nur Anzahl der Treffer
+mdgrep '"open source"' . --json > treffer.json # JSON für andere Programme
+mdgrep 'fehler' logs/ -g '*.txt' # andere Dateien als Markdown
+mdgrep 'rust' . -m 5 # nach 5 Treffern aufhören
+```
+
+Ausgabe (Fundstellen farbig, wenn die Ausgabe ein Terminal ist):
+
+```text
+notizen/sprachen.md:5
+ Rust guarantees memory safety without a garbage collector. Its borrow checker
+ rejects data races at compile time.
+```
+
+### 3.2 Optionen
+
+| Option | Bedeutung | Standard |
+|---|---|---|
+| `-s`, `--split MODUS` | Einheit, die durchsucht und angezeigt wird (siehe 3.3) | `paragraph` |
+| `-g`, `--glob MUSTER` | Dateimuster in Verzeichnissen | `*.md` |
+| `-c`, `--count` | nur Anzahl der Treffer-Blöcke ausgeben | |
+| `-l`, `--files-with-matches` | nur Dateinamen ausgeben | |
+| `-j`, `--json` | JSON-Ausgabe (gleiches Format wie die Web-API) | |
+| `-C`, `--context N` | Zeichen Kontext um Treffer bei langen Blöcken | `80` |
+| `--max-chars N` | Blöcke über N Zeichen kürzen (`0` = nie) | `400` |
+| `-m`, `--max-count N` | nach N Treffern aufhören (`0` = alle) | `0` |
+| `--timeout SEK` | Zeitlimit für die ganze Suche | `30` |
+| `--color auto\|always\|never` | Farbe (`NO_COLOR` wird beachtet) | `auto` |
+| `-V`, `--version` | Version anzeigen | |
+
+### 3.3 Split-Modi – was ist ein Treffer?
+
+| Modus | Ein Block ist … | Geeignet für |
+|---|---|---|
+| `paragraph` | ein durch Leerzeilen getrennter Absatz | Fließtext, Notizen |
+| `list` | ein Listenpunkt der obersten Ebene (mit eingerückten Unterpunkten); Text außerhalb von Listen wird zu Absätzen | Aufzählungen, Berichte, Changelogs |
+| `heading` | ein Abschnitt von Überschrift zu Überschrift | Dokumentation, Wikis |
+| `file` | eine ganze Datei | „In welchen Dateien kommt X und Y vor?" |
+
+Die logische Verknüpfung gilt **pro Block**: `rust AND go` findet nur Blöcke, in denen beide
+Wörter vorkommen. Mit `--split file` gilt sie für die ganze Datei.
+
+### 3.4 Rückgabewerte (für Skripte)
+
+| Code | Bedeutung |
+|---|---|
+| `0` | Treffer gefunden |
+| `1` | kein Treffer |
+| `2` | Fehler: Syntaxfehler (mit Markierung der Position), Datei nicht lesbar oder Zeitlimit erreicht |
+
+```bash
+if mdgrep 'TODO OR FIXME' src/ -c > /dev/null; then echo "Es gibt noch offene Punkte"; fi
+```
+
+---
+
+## 4. Python-Bibliothek
+
+### 4.1 Nur die Suchsprache
+
+```python
+from mdgrep import compile_query, QuerySyntaxError
+
+q = compile_query('(rust OR go) AND NOT /crypto(currency)?/')
+q.matches("Rust 2.0 is out") # True
+q.spans("Rust and Go") # [(0, 4), (9, 11)] → Positionen zum Hervorheben
+
+try:
+ compile_query("(rust OR go")
+except QuerySyntaxError as e:
+ print(e.code, e.position, e.message) # unclosed_paren 0 Missing closing parenthesis
+```
+
+- `compile_query(text, max_length=500, max_terms=50)` → `Query`
+- `Query.matches(text, timeout=None)` → `bool`
+- `Query.spans(text, timeout=None)` → Liste von `(start, ende)`, sortiert und zusammengefasst;
+ Begriffe unter NICHT werden nicht markiert
+- Bei Überschreitung von `timeout` (Sekunden) wird `QueryTimeout` ausgelöst
+
+### 4.2 Markdown-Dateien durchsuchen
+
+```python
+from mdgrep import load_markdown, SearchEngine
+
+blocks = load_markdown("docs/", split="list", relative_to="docs/")
+engine = SearchEngine(blocks)
+
+result = engine.search("rust -crypto", limit=20)
+print(result.total_blocks, "Treffer")
+for group in result.groups:
+ b = group.block
+ print(f"{b.source}:{b.line}", "".join(s["t"] for s in group.segments))
+```
+
+`load_markdown(pfade, ...)`:
+
+| Parameter | Bedeutung | Standard |
+|---|---|---|
+| `paths` | Datei, Verzeichnis oder Liste davon | |
+| `split` | `file` · `heading` · `list` · `paragraph` | `paragraph` |
+| `meta` | Funktion `pfad → dict`; wird in die Metadaten jedes Blocks der Datei übernommen | |
+| `relative_to` | Verzeichnis, relativ zu dem `source`/`id` angegeben werden (wichtig für Web: keine Serverpfade preisgeben) | absolute/angegebene Pfade |
+| `pattern` | Dateimuster in Verzeichnissen | `*.md` |
+| `dialect` | `commonmark` oder `python-markdown` (siehe 4.6) | `commonmark` |
+| `encoding` | Zeichenkodierung | `utf-8` |
+
+Jeder Block bekommt in `meta` automatisch: `section` (nächste Überschrift darüber),
+`kind` (`file`/`section`/`item`/`paragraph`) und `markdown` (Original-Quelltext).
+Durchsucht wird der Text **ohne** Markdown-Zeichen (Links, Fett, Code …).
+
+### 4.3 Eigene Daten durchsuchen
+
+mdgrep ist nicht auf Markdown beschränkt. Jede Datenquelle lässt sich als Liste von `Block`-Objekten
+übergeben:
+
+```python
+from mdgrep import Block, SearchEngine
+
+tickets = [
+ Block(id="42", title="Ticket 42", text="Login schlägt mit SSO fehl", meta={"status": "offen"}),
+ Block(id="43", title="Ticket 43", text="Dunkelmodus-Schalter defekt", meta={"status": "erledigt"}),
+]
+engine = SearchEngine(tickets)
+engine.search("login ODER sso", filters={"status": "offen"})
+```
+
+| Feld | Bedeutung |
+|---|---|
+| `id` | eindeutige Kennung |
+| `title` | wird durchsucht und als Überschrift angezeigt (nicht im `text` wiederholen) |
+| `text` | wird durchsucht und als Ausschnitt angezeigt |
+| `anchor` | optionale Sprungmarke (`#anchor`), Linkziel im Widget, wenn kein `href` gesetzt ist |
+| `source`, `line` | Herkunft (Datei, Zeile ab 1) |
+| `meta` | beliebiges Wörterbuch; wird unverändert durchgereicht, Filter arbeiten darauf |
+
+### 4.4 Suchoptionen
+
+```python
+result = engine.search(
+ "rust",
+ filters={"lang": ["de", "en"]}, # alle Schlüssel müssen passen; leere Werte = egal
+ predicate=lambda b: b.line > 1, # zusätzlicher eigener Filter
+ group_by=lambda b: b.meta.get("url"), # gleiche Schlüssel → ein Ergebnis, Rest in group.also
+ sort_key=lambda b: b.meta["date"], reverse=True, # neueste zuerst
+ offset=0, limit=30, # Seiten (zählt Gruppen)
+ time_budget=1.5, # Sekunden; danach Teilergebnis mit truncated=True
+ snippet_chars=700, context=120, # lange Texte auf Fenster um die Treffer kürzen
+)
+```
+
+Das Ergebnis (`SearchResult`):
+
+| Attribut | Bedeutung |
+|---|---|
+| `groups` | Treffer der aktuellen Seite; je Gruppe `block`, `segments`, `title_segments`, `hits`, `also` |
+| `total_groups`, `total_blocks` | Anzahl Ergebnisse bzw. Fundstellen insgesamt |
+| `next_offset` | `offset` der nächsten Seite oder `None` |
+| `truncated` | `True`, wenn das Zeitbudget aufgebraucht wurde |
+| `elapsed_ms` | Suchdauer |
+| `to_dict()` | JSON-fähiges Wörterbuch (Format siehe 5.3) |
+
+`segments` ist eine Liste `[{"t": "Text", "hit": False}, {"t": "Treffer", "hit": True}, …]` –
+fertig zum Anzeigen mit Hervorhebung.
+
+### 4.5 Automatisch neu laden
+
+Ändern sich die Dateien, lädt die Suchmaschine sie selbst neu – ohne Neustart:
+
+```python
+from pathlib import Path
+
+def stand(verzeichnis=Path("docs")):
+ dateien = [p.stat() for p in verzeichnis.rglob("*.md")]
+ return len(dateien), max((s.st_mtime for s in dateien), default=0)
+
+engine = SearchEngine(lambda: load_markdown("docs", relative_to="docs"),
+ reload_check=stand, reload_interval=60)
+```
+
+`reload_check` liefert einen beliebigen vergleichbaren Wert; ändert er sich, wird neu geladen.
+Geprüft wird höchstens alle `reload_interval` Sekunden. `engine.reload()` lädt sofort neu.
+
+### 4.6 Markdown-Dialekt – wenn Treffer auf HTML-Elemente verlinken
+
+Wer Treffer mit Sprungmarken in gerenderten HTML-Seiten verknüpft, braucht dieselbe Aufteilung wie
+der Markdown-Renderer. Die Renderer unterscheiden sich bei Listen:
+
+| | `commonmark` (Standard) | `python-markdown` |
+|---|---|---|
+| Liste direkt unter einer Absatzzeile (ohne Leerzeile) | beginnt eine Liste | gehört zum Absatz |
+| Unterpunkt mit 1–3 Leerzeichen Einrückung | verschachtelt | eigener Punkt auf oberster Ebene |
+
+```python
+split_markdown(text, split="list", dialect="python-markdown")
+```
+
+### 4.7 Weitere Funktionen
+
+| Funktion | Zweck |
+|---|---|
+| `split_markdown(text, source=…, split=…, meta=…, dialect=…)` | einen Markdown-Text (statt Dateien) zerlegen |
+| `markdown_to_text(md)` | Markdown-Zeichen entfernen, Zeilen erhalten |
+| `segments(text, spans, max_chars=…, context=…)` | Text + Fundstellen → Anzeige-Segmente |
+| `export_static(zielverzeichnis)` | Widget-Dateien (JS + CSS) kopieren, siehe 6.4 |
+
+---
+
+## 5. Web-API
+
+Benötigt `mdgrep[web]`.
+
+### 5.1 Eigenständige App
+
+```python
+# app.py
+from mdgrep import SearchEngine, load_markdown
+from mdgrep.web import create_app
+
+engine = SearchEngine(lambda: load_markdown("docs", split="list", relative_to="docs"))
+app = create_app(engine, filters=["kind"]) # GET /search?q=…&kind=…
+```
+
+### 5.2 In eine bestehende Flask-App einhängen
+
+```python
+from mdgrep.web import create_blueprint
+
+app.register_blueprint(create_blueprint(engine, filters=["lang"]), url_prefix="/api")
+# → GET /api/search?q=… und Widget-Dateien unter /api/mdgrep-static/
+```
+
+Optionen von `create_blueprint()` (zusätzlich zu `group_by`, `sort_key`, `reverse`,
+`time_budget`, `snippet_chars`, `context` aus 4.4):
+
+| Option | Bedeutung | Standard |
+|---|---|---|
+| `filters` | Metadaten-Schlüssel, die als URL-Parameter filterbar sind | keine |
+| `default_limit` / `max_limit` | Ergebnisse pro Seite / Obergrenze | `30` / `100` |
+| `max_query_length` | maximale Länge der Suchanfrage | `300` |
+| `hide_meta` | Metadaten-Schlüssel, die nicht ausgeliefert werden | `("markdown",)` |
+| `name` | Blueprint-Name (bei mehreren Suchen in einer App) | `mdgrep` |
+
+`create_app(engine, url_prefix="", static_dir=None, **optionen)` – mit `static_dir` liefert die App
+zusätzlich die Dateien dieses Verzeichnisses unter `/` aus (praktisch zum Testen einer statischen Seite).
+
+### 5.3 Anfragen und Antworten
+
+```text
+GET /search?q=rust%20-crypto&lang=de&offset=0&limit=30
+```
+
+- `q` – Suchanfrage (leer → leeres Ergebnis)
+- je Name in `filters` ein Parameter: leer oder `all` = keine Einschränkung, `de,en` = mehrere Werte
+- `offset`, `limit` – Seiten
+
+Antwort (HTTP 200):
+
+```json
+{
+ "query": "rust -crypto", "total_groups": 12, "total_blocks": 15,
+ "offset": 0, "limit": 30, "next_offset": null, "truncated": false, "elapsed_ms": 3.2,
+ "results": [{
+ "block": {
+ "id": "a.md:4", "title": "…", "anchor": "", "source": "a.md", "line": 4, "meta": {},
+ "title_segments": [{"t": "…", "hit": false}],
+ "segments": [{"t": "The ", "hit": false}, {"t": "Rust", "hit": true}, {"t": " team …", "hit": false}],
+ "hits": 1
+ },
+ "also": [{"id": "b.md:9", "title": "…", "anchor": "", "source": "b.md", "line": 9, "meta": {}}]
+ }]
+}
+```
+
+Fehler (HTTP 400):
+
+```json
+{"error": {"code": "unclosed_paren", "message": "Missing closing parenthesis", "position": 0}}
+```
+
+### 5.4 Entwicklungsserver
+
+```bash
+python -m mdgrep.web --dir docs --split list --static site --port 8000
+# → http://127.0.0.1:8000/ (Dateien aus site/)
+# → http://127.0.0.1:8000/api/search (API)
+```
+
+| Option | Bedeutung | Standard |
+|---|---|---|
+| `--dir` | Verzeichnis mit Markdown-Dateien (Pflicht) | |
+| `--split` | Split-Modus | `paragraph` |
+| `--static` | dieses Verzeichnis unter `/` ausliefern; API dann unter `--api-prefix` | |
+| `--api-prefix` | Pfad der API, wenn `--static` gesetzt ist | `/api` |
+| `--filters` | kommagetrennte Metadaten-Schlüssel als Filter, z. B. `kind,section` | |
+| `--host`, `--port` | Adresse | `127.0.0.1`, `8000` |
+
+Neue oder geänderte Dateien werden im Entwicklungsserver nach spätestens 5 Sekunden übernommen.
+Ein vollständiges Beispiel liegt in `examples/`:
+
+```bash
+python -m mdgrep.web --dir examples/docs --split list --static examples/site
+```
+
+---
+
+## 6. Such-Widget
+
+Eine fertige Suchmaske: Ergebnisse erscheinen beim Tippen, veraltete Anfragen werden abgebrochen,
+die Suche steht in der URL (teilbar, Zurück-Taste funktioniert), Filter, „Mehr laden",
+aufklappbare Syntaxhilfe, Fehlermeldungen mit Position, Beschriftung Deutsch oder Englisch.
+Alle Texte werden sicher als Text eingefügt (kein HTML vom Server → kein Cross-Site-Scripting).
+Keine Abhängigkeiten, kein Framework.
+
+### 6.1 Einbinden
+
+```html
+
+