docs: Beschriftungs-Spec am Prototyp gegen die echte Vorlage geschärft

Straßenenden über das Skelett samt Maske, Korridor statt Versatzgrenze,
Weiterführen über Kreuzungen, Ziel auf der anderen Seite, keine gemalten
Sackgassen, Kartenrand; Gebäudevorlage ab 100 m² im 150-m-Fenster;
randvoll vor dem Wachsen; Kontrastregel der Suche; Probelauf mit Kennzahlen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Opus 5 committed 2026-09-13 17:53:13 +02:00
1 parent 57ffb5953a
commit ac2f414bb6
1 file changed
+44 -26
@@ -21,6 +21,8 @@ Gemessen an der Vorlage (`tools/extract/cache`, YANIS v15) und den ausgelieferte
- **Folgeschäden in L8:** Marker S2/32 ergab ein Brückenstück mit Kollision mitten in der Stadt (285 m², Welt x 1911 / y 628), die Marker T2/65 und T2/51 zwei Löcher in der Landplatte (268 m² bei x 1918 / y 593, 172 m² bei x 1912 / y 620).
- **Unter großen Schriftzügen** (Maske = Füllung + 7 m): VICE BEACH 10 Straßenenden, kein angeschnittenes Gebäude ≥ 60 m²; CROSSTOWN 9 Straßenenden, 8 angeschnittene Gebäude; STOCKYARD 12 und 2; BAYSIDE 6 und 3; OCEAN BEACH 5 und 2; BOHEMIA 0 und 1.
- **Farbgleiche echte Flächen:** rosa Gebäudekomplex D2, rote Parkwege C6 und C11, lachsrote Rechtecke B8 und B9, knallrote Gebäude A10, K10 und L10, rote spekulative Autobahnrampen A12. Eine Farbsuche über die ganze Karte würde sie mit löschen.
- **Buchstaben-Häuser:** Rote Schriftzüge und rote Marker liegen farblich am nächsten an „spekulativ“ und werden heute zu spekulativen Gebäuden — in A1 stehen 18 davon aus den Buchstaben von SHA-WOCKA, in G9 14, in D1 13.
- **Blasse und winzige Beschriftungen:** „VICE-DALE COUNTY (MIAMI DADE)“ ist halb durchsichtig (Helligkeit 224–240 über Stadtfläche bis 230); die kleinen „Shore Ct“ haben Grauwerte 150–210 wie Gebäude (176); Golf-Fähnchen sind dünne dunkle Striche. Keine Farbregel trennt sie sicher von der Karte.
- **Keine beschriftungsfreie Vorlage:** gtadb führt nur andere Rekonstruktionen, keinen Label-Layer (Ruling 33).
- **Reproduzierbar:** `run.py` erzeugt die ausgelieferten 144 Chunk-Dateien und `manifest.json` in 23 s Byte für Byte identisch neu.
@@ -34,6 +36,7 @@ Gemessen an der Vorlage (`tools/extract/cache`, YANIS v15) und den ausgelieferte
| Probe vorab | Keine |
| Knallrote Flächen A10, K10, L10 | Echte Gebäude, bleiben stehen |
| Einbau, Liste und Erkennung · Wiederaufbau · Fehlerfälle und Prüfung | Abschnitte 1 bis 3 bestätigt |
| Spec | Freigegeben („leg los“); danach am Wegwerf-Prototyp gegen die echte Vorlage geschärft — siehe „Probelauf“ |
## Einbau in die Pipeline
@@ -90,7 +93,7 @@ reason = "knallrotes Gebäude in A10, laut Nutzer echt"
| `[[keep]]` | `box` | ja | Stelle, die wie Beschriftung aussieht, aber echt ist. Wirkt nur auf `find_annotations.py --check`. |
| | `reason` | ja | Warum sie echt ist. |
Der Rahmen soll die Beschriftung mit mindestens 8 m Luft umschließen; `--propose` schlägt Rahmen mit 10 m Luft vor.
Der Rahmen soll die Beschriftung mit mindestens 8 m Luft umschließen; `--propose` schlägt Rahmen mit 10 m Luft vor. Wo keine Farbregel sicher greift — blasse Schrift, winzige Straßennamen in Gebäudegrau, Piktogramme aus dünnen dunklen Strichen —, bekommt die Stelle einen engen `region`-Rahmen: dann wird der ganze Rahmen neu aufgebaut.
## Erkennung je Art
@@ -107,8 +110,8 @@ Je Eintrag entsteht eine Maske in fünf Schritten:
| `text_white` | min(R, G, B) ≥ 236 und max − min ≤ 14 | 7 m |
| `text_red` | ΔE zu (248, 112, 112) < 18 — euklidischer Abstand in CIELAB wie in `classify.py` | 7 m |
| `marker` | HSV-Sättigung ≥ 0,6, Helligkeit ≥ 0,5 (Skala 0–1) und ΔE ≥ 25 zu jeder Palettenfarbe aus `classify.py` — oder fast schwarz: max(R, G, B) ≤ 45 | 3 m |
| `symbol` | wie `text_white` **oder** wie `marker` | 3 m |
| `street_name` | min(R, G, B) ≥ 150 und max − min ≤ 20 | 1 m |
| `symbol` | wie `text_white` **oder** wie `marker` | 4 m — der dunkelblaue Rand der Schilder erfüllt keine Farbregel |
| `street_name` | min(R, G, B) ≥ 200 und max − min ≤ 20 — Gebäudegrau (176) liegt darunter | 1 m |
| `region` | ganzer Rahmen oder ganzes Vieleck, keine Farbregel, kein Schritt 2 bis 4 | 0 m |
Die **Gitternetz-Maske** sind die Streifen aus `[grid]`, auf den Ausschnitt beschnitten.
@@ -117,23 +120,25 @@ Die **Gitternetz-Maske** sind die Streifen aus `[grid]`, auf den Ausschnitt besc
Reihenfolge in `inpaint.py`:
1. **Gitternetz:** Jedes Pixel der Gitternetz-Maske, das in keiner Eintragsmaske liegt, übernimmt die Klasse des nächsten Pixels außerhalb aller Masken, gleich welcher Klasse.
1. **Gitternetz:** Jedes Pixel der Gitternetz-Maske, das in keiner Eintragsmaske liegt, übernimmt die Klasse des nächsten Pixels außerhalb aller Masken, gleich welcher Klasse. Gefüllt wird in Streifen von 10 m beiderseits jeder Linie.
2. **Eintragsmasken:** Alle Eintragsmasken werden vereinigt und in zusammenhängende Masken zerlegt; überlappende Einträge landen so in derselben Maske. Je Maske:
- **dünn** (größter Abstand eines Maskenpixels zum Maskenrand ≤ 2 m — Straßennamen, Zeigerlinien): wie das Gitternetz.
- **sonst:** Straßen und Wege → Flächen → Gebäude schließen → Gebäude ergänzen, jeweils in einem Fenster aus dem Hüllrechteck der Maske plus 60 m.
- **sonst:** Straßen und Wege → Flächen → Gebäude schließen in einem Fenster aus dem Hüllrechteck der Maske plus 60 m, danach Gebäude ergänzen im Nachbarschaftsfenster (plus 150 m).
### Straßen und Wege
Je Straßenklasse getrennt, in der Reihenfolge `weg`, `spekulativ`, `strasse` — was später gemalt wird, gewinnt.
Geplant wird je Straßenklasse in der Reihenfolge `strasse`, `spekulativ`, `weg`, damit niedrigere Klassen an höheren anschließen; gemalt wird umgekehrt — `weg`, `spekulativ`, `strasse` —, damit an Kreuzungen die Straße gewinnt.
- **Messen:** Die sichtbaren Pixel der Klasse (außerhalb aller Masken) werden mit `_skeleton_graph` und `_skeleton_paths` aus `vectors.py` zu Mittellinien. Vertraut wird nur Mittellinienpunkten mit mindestens **10 m** Abstand zur Maske. Wo ein Pfad in diese 10-m-Zone eintritt, entsteht ein **Straßenende**: Ort = letzter vertrauter Punkt, Richtung = von dem Punkt 15 m weiter zurück zum Ende (bei kürzerem vertrauten Stück dessen ganze Länge; unter 5 m kein Ende), Halbbreite = Median der Distanztransformation entlang dieser 15 m. Ein Ende zählt nur, wenn der Strahl von dort in seiner Richtung die Maske innerhalb von 20 m erreicht — eine Straße, die bloß an der Maske vorbeiführt, erzeugt keine Enden.
- **Verbinden:** Zwei Enden `a`, `b` derselben Maske passen, wenn die Richtung jedes Endes höchstens **20°** von der Verbindungslinie abweicht, der seitliche Versatz jedes Endes zur Linie des anderen höchstens `max(2 · größere Halbbreite, 0,15 · Abstand)` beträgt und die Halbbreiten sich höchstens um den Faktor **1,6** unterscheiden. Kosten: Summe der beiden Winkel in Grad plus 100 · Versatz / Abstand. Paare werden aufsteigend nach Kosten vergeben, jedes Ende höchstens einmal. Ein Paar wird als kubische Hermite-Kurve gemalt, die an beiden Enden deren Richtung aufnimmt; die Halbbreite geht linear von `a` nach `b` über.
- **Weiterführen:** Ein Ende ohne Partner läuft geradeaus. Treffer zählen erst ab dem ersten Maskenpixel — vorher läuft der Strahl noch über die eigene sichtbare Straße. Er endet am ersten dieser Treffer: an der Mittellinie eines gemalten Paares (T-Kreuzung), am Schnittpunkt mit einem anderen weitergeführten Ende (beide enden dort — Ecke ohne Überstand), außerhalb der Maske auf einem Pixel der eigenen Klasse oder `strasse` (angeschlossen), oder außerhalb der Maske auf einem anderen Pixel — dann am Maskenrand (**Sackgasse**). Endet es an einer Kreuzung, wird es um die eigene Halbbreite verlängert, damit die Ecke gefüllt ist.
- **Messen:** Die sichtbaren Pixel der Klasse (außerhalb aller Masken) werden zusammen mit der Maske skelettiert — die Maske zählt dabei als Straße, sonst schrumpfte die Mittellinie einer abgeschnittenen Straße um ihre Halbbreite und erreichte die Zone nie —, danach wird die Maske wieder entfernt und `_skeleton_graph` aus `vectors.py` macht daraus einen Graphen. Vertraut wird nur Mittellinienpunkten mit mindestens **10 m** Abstand zur Maske. Ein vertrauter Punkt mit einem Nachbarn in der 10-m-Zone ist ein **Straßenende**. Richtung: der Mittellinie 15 m von der Maske weg folgen, an Verzweigungen möglichst geradeaus, und vom Endpunkt dieser Spur zum Ende zeigen (unter 5 m Spur kein Ende). Halbbreite: Median der Distanztransformation auf den ersten **5 m** der Spur — weiter hinten liegt oft schon die Kreuzung mit einer breiteren Straße. Ein Ende zählt nur, wenn innerhalb von 20 m vor ihm Maske höchstens eine Halbbreite neben dem Strahl liegt; so zählt auch eine seitlich angeschnittene Straße, eine bloß vorbeiführende nicht. Enden näher als 8 m am Rand des Ausschnitts zählen nicht — dort ist das Skelett unzuverlässig.
- **Verbinden:** Zwei Enden `a`, `b` derselben Maske passen, wenn die Richtung jedes Endes höchstens **20°** von der Verbindungslinie abweicht und die Halbbreiten sich höchstens um den Faktor **1,6** unterscheiden. Die Verbindung ist eine kubische Hermite-Kurve, die an beiden Enden deren Richtung aufnimmt; mindestens **90 %** ihrer Punkte müssen auf Maske oder sichtbarer Straße liegen (Korridor). Paare werden aufsteigend nach der Summe der beiden Winkel vergeben, jedes Ende höchstens einmal. Der Korridor ersetzt eine Grenze für den seitlichen Versatz: am Prototyp waren die Richtungen breiter Straßen nahe Kreuzungen um bis zu 20° schief, und zwei parallele Straßen scheitern ohnehin am sichtbaren Block zwischen ihnen.
- **Weiterführen:** Ein Ende ohne Partner läuft geradeaus. „In der Maske“ heißt dabei: Maske höchstens eine Halbbreite neben der Mittellinie. Ein **Anschluss** ist eine schon geplante Mittellinie in der Maske (Kreuzung) oder sichtbare Straße der eigenen Klasse oder `strasse` höchstens eine Halbbreite neben dem Strahl — Buchstabenkonturen in der Maske zählen nicht. Der Strahl läuft von Anschluss zu Anschluss weiter, solange der nächste höchstens **60 m** entfernt ist, und endet am letzten: im Straßenraster geht eine Straße an einer Kreuzung meist weiter. Erreicht er in der Maske den Rand des Ausschnitts, schließt er dort an. Verlässt er die Maske auf anderem Boden, ist Schluss. Das runde Strichende reicht eine Halbbreite über den Endpunkt, damit Ecken und T-Kreuzungen ohne Überstand gefüllt sind. Kreuzen sich zwei weitergeführte Strahlen, enden beide am Schnittpunkt.
- **Ziel auf der anderen Seite:** Ein Ende, das so keinen Anschluss findet, nimmt als Ziel den Mittellinienpunkt derselben Klasse in der 10-m-Zone, dessen Richtung am wenigsten abweicht (höchstens 20°), sofern die Kurve dorthin mindestens 3 Punkte in der Maske hat und den Korridor erfüllt — so läuft ein Weg durch, dessen anderes Ende keine brauchbare Richtung hat.
- **Sackgasse:** Ein Ende ohne Anschluss und ohne Ziel wird gezählt, aber nicht gemalt.
- **Malen:** Nur Pixel innerhalb der Maske werden gesetzt.
### Flächen
Jedes Maskenpixel, das keine Straße bekommen hat, erhält die Klasse des nächsten Pixels außerhalb aller Masken, dessen Klasse `wasser`, `land`, `gruen`, `sand` oder `stadt` ist. Straßen und Gebäude wachsen dabei nicht.
Jedes Maskenpixel, das keine Straße bekommen hat, erhält die Klasse des nächsten Pixels außerhalb aller Masken, dessen Klasse `wasser`, `land`, `gruen`, `sand` oder `stadt` ist. Als Quelle zählen nur Flächenstücke ab 50 m² — kleinere sind Kantenglättung und ergäben Sandflecken im Park. Straßen und Gebäude wachsen dabei nicht.
### Gebäude schließen
@@ -142,11 +147,11 @@ Ein Gebäudestück — zusammenhängende `gebaeude`-Pixel außerhalb aller Maske
### Gebäude ergänzen
- **Bebaubar** sind Maskenpixel mit der Klasse `stadt` nach dem Flächenschritt, mindestens den Straßenabstand (siehe Vorlage) von jeder Straße und mindestens 3 m von jedem Gebäude entfernt — auch von gerade erst gesetzten.
- **Blöcke** sind die zusammenhängenden Flächen (4er-Nachbarschaft) im Fenster, die weder Straße, Weg, spekulativ noch Wasser sind.
- **Nachbarschaft** sind die Gebäudestücke außerhalb aller Masken, die höchstens **150 m** von der Maske entfernt liegen. Gibt es weniger als drei, wird in dieser Maske nichts ergänzt; der Bericht nennt das.
- **Blöcke** sind die zusammenhängenden Flächen (4er-Nachbarschaft) im Nachbarschaftsfenster, die weder Straße, Weg, spekulativ noch Wasser sind. Im 60-m-Fenster wäre ein Block oft mitten durchgeschnitten und sein sichtbar bebauter Teil läge draußen.
- **Nachbarschaft** sind die Gebäudestücke ab **100 m²** außerhalb aller Masken, die höchstens **150 m** von der Maske entfernt liegen. Kleinere Stücke sind Kantenreste und Nebengebäude; mit ihnen läge der Median bei 9 × 19 m und es entstünden Dutzende Häuschen. Gibt es weniger als drei, wird in dieser Maske nichts ergänzt; der Bericht nennt das.
- **Vorlage aus der Nachbarschaft:** kurze Seite = Median der kurzen Hüllrechteckseiten, auf 6–40 m begrenzt; lange Seite = Median der langen, auf kurze Seite bis 60 m begrenzt; Straßenabstand = Median des Abstands der Gebäudepixel am Umriss zur nächsten Straße, auf 2–15 m begrenzt.
- **Dichte je Block:** Anteil der Gebäudepixel am sichtbaren Teil des Blocks, sofern dieser mindestens 25 % des Blocks und 400 m² umfasst; sonst die Gebäudedichte der Nachbarschaft. **Ziel** = Dichte · verdeckte Blockfläche − dort schon geschlossene Gebäudefläche. Liegt das Ziel unter der halben Fläche eines Vorlage-Rechtecks, wird im Block nichts gesetzt.
- **Setzen:** Ausrichtung entlang der langen Seite des gedrehten Hüllrechtecks des Blocks. Die Reihen laufen parallel dazu, beginnend an der Hüllrechteckseite mit den meisten Straßenpixeln, die erste im Straßenabstand, die weiteren im Abstand kurze Seite + 3 m. In jeder Reihe folgen Rechtecke der Vorlagengröße mit 3 m Lücke. Ein Rechteck wird gesetzt, wenn alle seine Pixel bebaubar sind; gesetzt wird in Reihen- und Positionsreihenfolge, bis das Ziel erreicht ist. Kein Zufall — gleiche Eingabe, gleiches Ergebnis.
- **Dichte je Block:** Anteil der Gebäudepixel am sichtbaren Teil des Blocks, sofern dieser mindestens 25 % des Blocks und 400 m² umfasst; sonst die Gebäudedichte (Gebäude- zu Nicht-Straßen-, Nicht-Wasser-Pixeln) im Umkreis von 150 m. **Ziel** = Dichte · verdeckte Blockfläche − dort schon geschlossene Gebäudefläche. Liegt das Ziel unter der halben Fläche eines Vorlage-Rechtecks, wird im Block nichts gesetzt.
- **Setzen:** Ausrichtung entlang der langen Seite des gedrehten Hüllrechtecks des Blocks. Die Reihen laufen parallel dazu und beginnen an der langen Hüllrechteckseite mit den meisten Straßenpixeln bis Straßenabstand + 2 m davor. Die erste Reihe liegt im Straßenabstand; passt in einer Reihe nichts, rückt sie 1 m nach innen, sonst folgt die nächste im Abstand kurze Seite + 3 m. In der Reihe rückt die Position meterweise vor, bis ein Rechteck der Vorlagengröße passt, danach um lange Seite + 3 m. Ein Rechteck passt, wenn alle seine Pixel bebaubar sind. Abgesucht wird nur, wo im Block bebaubare Pixel liegen, und nur bis das Ziel erreicht ist. Kein Zufall — gleiche Eingabe, gleiches Ergebnis.
- **Markierung:** `remove_annotations` gibt die gesetzten Rechtecke als Weltpolygone zurück. `run.py` setzt nach der Höhenzuweisung `source = "annotation"` bei jedem Gebäude, das zu mindestens 50 % seiner Fläche in diesen Rechtecken liegt. Die Höhe bleibt die aus `assign_heights`. Der Browser liest `source` nicht.
## Fehlerfälle
@@ -160,8 +165,8 @@ Ein Gebäudestück — zusammenhängende `gebaeude`-Pixel außerhalb aller Maske
**Inhaltliche Probleme — Warnung, der Lauf geht weiter:**
- **leer:** Die Farbregel findet nach Schritt 2 kein Pixel. Eintrag übersprungen.
- **randvoll:** Die Maske füllt mehr als 60 % des Rahmens — Rahmen zu eng oder Art falsch. Eintrag übersprungen; gilt nicht für `region`.
- **angeschnitten:** Ein in Schritt 2 verworfener Kandidat hat mindestens 20 Pixel — vermutlich schneidet der Rahmen Schrift ab. Eintrag wird angewendet.
- **randvoll:** Die Farbpixel nach Schritt 2 — vor dem Wachsen — füllen mehr als 60 % des Rahmens: Rahmen zu eng oder Art falsch. Eintrag übersprungen; gilt nicht für `region`. Gemessen wird vor dem Wachsen, weil ein richtig erkannter Schriftzug mit 7 m Rand und gefüllten Innenräumen am Prototyp regulär 60–77 % seines Rahmens bedeckte.
- **angeschnitten:** Ein in Schritt 2 verworfener Kandidat hat mindestens 20 Pixel — vermutlich schneidet der Rahmen Schrift ab. Eintrag wird angewendet. Nicht bei `street_name`: dessen Farbregel trifft auch die Stadtfläche neben der Fahrbahn, und die berührt immer den Rand.
Die Farbanteil-Grenzen in `run.py` bleiben unverändert und prüfen das bereinigte Klassenbild.
@@ -170,31 +175,35 @@ Die Farbanteil-Grenzen in `run.py` bleiben unverändert und prüfen das bereinig
`run.py` druckt nach `remove_annotations`:
```
Beschriftungen: 91 angewendet, 0 übersprungen · Gitternetz: 14 Linien
#17 VICE BEACH (MIAMI BEACH) · K7 · Maske 44.223 m² · Straßen 5 verbunden, 1 weitergeführt, 0 Sackgassen · Gebäude 0 geschlossen, 3 ergänzt
Beschriftungen: 107 angewendet, 0 übersprungen · Gitternetz: 14 Linien
#64 VICE BEACH (MIAMI BEACH) · K7 · Maske 46.296 m² · Straßen 7 verbunden, 1 weitergeführt, 0 Sackgassen · Gebäude 0 geschlossen, 2 ergänzt
#87 (BAYFRONT) · C10 · Maske 8.926 m² · gemeinsam mit #86
Warnungen:
#52 L1/42 · C6 · angeschnitten: Rahmen prüfen
```
Der Chunk ist der des Rahmenmittelpunkts über `chunk_for_world`. Zählt eine Maske mehrere Einträge, stehen Straßen- und Gebäudezahlen beim ersten.
Der Chunk ist der des Rahmenmittelpunkts über `chunk_for_world`. Zählt eine Maske mehrere Einträge, stehen Straßen- und Gebäudezahlen beim ersten, die übrigen verweisen mit „gemeinsam mit #N“ darauf; gleiche Hinweise wie „zu wenig Nachbarn“ stehen nur einmal da. Danach druckt `run.py` die Gebäudezahl samt der ergänzten.
## Werkzeug `find_annotations.py`
- `--propose` durchsucht die ganze Vorlage mit den Farbregeln, fasst Treffer zu Fundstellen zusammen und schreibt einen Listenentwurf mit Rahmen (10 m Luft) und geschätzter Art nach `debug/annotations/vorschlag.toml`, dazu einen nummerierten Kontaktbogen. Die Liste in `data/` wird von Hand daraus übernommen und benannt.
- `--check` sucht genauso und endet mit Status 1, wenn der Mittelpunkt einer Fundstelle in keinem `annotation`- und keinem `keep`-Rahmen liegt; es nennt jede solche Stelle mit Chunk und Koordinate. Die Suchregeln werden an der heutigen Vorlage so eingestellt, dass sie die 94 Fundstellen dieses Entwurfs finden und keine Küstenlinie melden — der hellblaue Küstenverlauf und die grünen Uferstreifen erfüllen die reine Marker-Farbregel.
- `--review` rendert je Eintrag `debug/annotations/NNN-name.png`: links die Vorlage mit Maskenumriss, rechts das Ergebnis aus den Chunk-Dateien in den Farben von `overview.py`, beschriftet mit Nummer, Name und Chunk; dazu `debug/annotations/uebersicht.png`.
- **Suche:** Die Farbregeln von `text_white`, `text_red` und `marker` laufen kachelweise (500 px) über die ganze Vorlage; Marker-Pixel brauchen zusätzlich einen Kontrast von mindestens 12 — Spannweite der CIELAB-Helligkeit im 3×3-Umfeld. Am echten Mosaik gemessen: Küstenverlauf und Uferstreifen erreichen im 95. Perzentil 9, Markerschrift liegt bei 95 % ihrer Pixel über 13. Treffer einer Art im Abstand bis 8 px bilden eine Fundstelle; Mindestgröße 40 Pixel für Schrift, 25 für Marker. Am Prototyp: 149 Fundstellen, keine Küstenlinie, die Tennisschläger fehlen (zu klein) und stehen deshalb von Hand in der Liste.
- `--propose` schreibt aus der Suche einen Listenentwurf mit Rahmen (10 m Luft) und der Art des Treffers nach `debug/annotations/vorschlag.toml`, dazu einen nummerierten Kontaktbogen `vorschlag.png`. Die Liste in `data/` wird von Hand daraus übernommen, benannt und nach Augenschein in Arten eingeteilt — Straßennamen, Schilder und Symbole findet die Suche als weiße oder Marker-Treffer.
- `--check` sucht genauso und endet mit Status 1, wenn der Mittelpunkt einer Fundstelle in keinem `annotation`- und keinem `keep`-Rahmen liegt; es nennt jede solche Stelle mit Art, Chunk und Koordinate.
- `--review` rendert je Eintrag `debug/annotations/NNN-name.png`: links die Vorlage mit Maskenumriss, rechts das Ergebnis aus `public/data/vice-a/map.png`, beschriftet mit Nummer, Name, Chunk und Art; dazu `debug/annotations/uebersicht.png`.
## Prüfung
**Tests (pytest, künstliche Bilder, ohne Kachel-Cache):**
- Liste: fehlende oder kommentarleere Datei ergibt keine Einträge; jeder Formfehler nennt Nummer und Feld; `keep` ohne `reason` ist ein Formfehler.
- Erkennung: weiße Schrift mit Kontur über Straße und Park wird samt Kontur erfasst; ein farbgleiches Stück am Rahmenrand bleibt; ein Buchstabeninnenraum wird gefüllt; `region` mit Vieleck maskiert genau das Vieleck; die Gitterstreifen liegen auf den richtigen Zeilen und Spalten; leer, randvoll und angeschnitten werden gemeldet.
- Dünne Masken: eine Straße längs unter einer Gitterlinie bleibt durchgehend; ein Park mit Gitterlinie hat keinen Schlitz.
- Straßen: eine gerade Straße durch ein Schriftband läuft in gleicher Breite weiter; eine leicht gekrümmte wird glatt verbunden; zwei Straßen mit Versatz über der Grenze werden nicht verbunden; eine Ecke unter der Maske entsteht ohne Überstand; eine T-Kreuzung schließt an; eine seitlich angeschnittene Straße behält ihre volle Breite; ein Weg bleibt Weg und wird an einer Kreuzung von der Straße überdeckt; ein Ende ohne Gegenüber wird als Sackgasse gezählt.
- Flächen: Park unter Schrift bleibt Park, Wasser bleibt Wasser, Straßen und Gebäude wachsen nicht.
- Erkennung: die Farbregeln treffen genau ihre Farben (Weiß, Lachsrot, Marker, Schwarz, helle Schrift) und nicht Stadtfläche, Gebäudegrau, Wasser, Park oder dunkles Spekulativ-Rot; weiße Schrift mit Kontur über Straße und Park wird samt Kontur erfasst; ein farbgleiches Stück am Rahmenrand bleibt und wird als angeschnitten gemeldet; ein Buchstabeninnenraum wird gefüllt, eine große eingeschlossene Fläche nicht; `region` mit Vieleck maskiert genau das Vieleck; die Gitterstreifen liegen auf den richtigen Zeilen und Spalten; leer und randvoll werden gemeldet; ein Schriftzug, der nach dem Wachsen mehr als 60 % des Rahmens bedeckt, gilt nicht als randvoll; ein Straßenname meldet die Stadtfläche am Rand nicht.
- Entfernen: ohne Einträge bleibt das Klassenbild unverändert und der Bericht lautet „0 angewendet“; eine Straße unter weißer Schrift läuft nach `remove_annotations` durch; das Gitternetz wird vor allem anderen geschlossen; zwei Einträge in einer Maske melden „gemeinsam mit“.
- Dünne Masken: eine Straße längs unter einer Gitterlinie bleibt durchgehend; ein Park mit Gitterlinie hat keinen Schlitz; bis 4 m Dicke gilt eine Maske als dünn.
- Straßen: eine gerade Straße durch ein Schriftband läuft in gleicher Breite weiter; ein leichter Versatz wird glatt verbunden; zwei Straßen über sichtbarem Boden hinweg werden nicht verbunden und als Sackgassen gezählt, aber nicht gemalt; eine Ecke unter der Maske entsteht ohne Überstand; eine Seitenstraße schließt an eine verdeckte Durchgangsstraße an; eine seitlich angeschnittene Straße behält ihre volle Breite; ein Weg bleibt Weg und wird an einer Kreuzung von der Straße überdeckt; eine Straße, die unter Schrift aus der Karte läuft, bleibt bis zum Rand erhalten.
- Flächen: Park unter Schrift bleibt Park, Wasser bleibt Wasser, Straßen und Gebäude wachsen nicht; ein Sandkrümel am Maskenrand färbt den Park nicht.
- Gebäude: ein angebissenes Gebäude wird zum Rechteck geschlossen, aber nicht länger; ein verdeckter Blockteil erreicht die Dichte des sichtbaren Teils auf ein Vorlage-Rechteck genau; im Park und im Wasser entsteht nichts; Straßenabstand und 3-m-Lücken werden eingehalten; jedes Rechteck liegt ganz im verdeckten Teil; unter drei Nachbarn entsteht nichts; zweimal dieselbe Eingabe ergibt dasselbe Ergebnis.
- Markierung: ein Gebäude mit ≥ 50 % Fläche in einem ergänzten Rechteck bekommt `source = "annotation"`, eines mit weniger behält seine Quelle.
- `chunk_for_world`: dieselben Beispiele wie `tests/coords.test.js`.
- Werkzeug: die Suche findet Schrift und Marker, aber keinen weichen Küstenverlauf; `--check` beachtet `annotation`- und `keep`-Rahmen; der Vorschlag ist eine gültige Liste mit 10 m Luft; `--review` schreibt eine Seite je Eintrag und die Übersicht.
**An den echten Daten, in dieser Reihenfolge:**
1. **Nullauf:** Ohne `annotations.toml` erzeugt `run.py` die ausgelieferten Chunk-Dateien, `manifest.json` und die Minimap-Dateien Byte für Byte identisch.
@@ -205,6 +214,15 @@ Der Chunk ist der des Rahmenmittelpunkts über `chunk_for_world`. Zählt eine Ma
6. **Im Spiel:** Minimap zeigt keine Schrift mehr; Fahrt durch VICE BEACH und CROSSTOWN mit Screenshots.
7. `cd tools/extract && uv run pytest -q` und `npm test` sauber.
## Probelauf
Vor dem Plan lief das ganze Verfahren als Wegwerf-Prototyp gegen die echte Vorlage; die Regeln oben sind der Stand danach.
- **Liste:** 107 Einträge — 25 Schriftzüge (18 weiß, 7 rot), 26 Schilder und Symbole, 44 Marker, 8 Straßennamen, 4 `region` (VICE-DALE COUNTY, Golf-Fähnchen, zweimal „Shore Ct“) — und 10 Ausnahmen. `--check` ohne offene Stelle, kein Eintrag mit Warnung.
- **Lauf:** 37 s statt 23 s, alle 107 angewendet.
- **Kennzahlen vorher → nachher:** Brücken 23 → 14, davon in L8 1 → 0; Löcher unter 2.000 m² in der Landplatte 35 → 13, davon in L8 2 → 0; Gebäudeeinträge über alle Chunks 1.895 → 1.588 — die Differenz sind vor allem die Buchstaben-Häuser aus roter Schrift und roten Markern; 13 Gebäude ergänzt (15 Chunk-Einträge); Farbanteile innerhalb der Grenzen.
- **Sichtprüfung:** Keine Schrift mehr auf der Gesamtkarte, das Gitternetz ist verschwunden. VICE BEACH: Park und Nord-Süd-Weg frei, alle vier Straßen und die Querstraße unter „CH)“ durchgehend, zwei ergänzte Gebäude. CROSSTOWN: angebissene Gebäude geschlossen, zwei ergänzt. Bekannte Reste für die Feinkalibrierung: vereinzelte Kantenzacken an Straßenrändern, Fahrspuren der Autobahn unter STOCKYARD, kurze rote Brückenstücke dort, wo Schilder auf Brücken über Wasser standen (die Schildscheibe war vorher als Landinsel eingelesen).
## Folgen für andere Dokumente
- `docs/superpowers/specs/2026-09-13-feinkalibrierung-design.md` und `docs/superpowers/plans/2026-09-13-feinkalibrierung.md`: Die Ausführungsschranke nennt diesen Entwurf statt task_999fed4a. `chunk_for_world` ist dann vorhanden; Aufgabe 1 des Plans legt die Funktion nicht erneut an. Fehler des Wiederaufbaus an einzelnen Stellen werden dort über `corrections.toml` behoben.