Add Hesse as an extraction and recognition case

The Hesse GVBl issue 5/2026 amends the ordinance on traffic-law
competences. All 21 commands of its article 1 are now recognised.

The blocked letterspacing that was expected to be the hard part turns
out to affect only the signature block, not a single command. What did
need work was three other things: the running GVBl footer and the
masthead remainder are now stripped as page furniture, and so is the
heading's asterisk footnote ("* Ändert FFN 61-60"), which sits at the
foot of a page between two enumeration items and stuck to the command
above it.

Two command forms are new, both general rather than Hessian: "am Ende"
may be absent from a punctuation replacement — the definite article then
requires the unit to carry exactly one such mark, which the applier
checks — and the object after "durch" may be elided in the form without
a location, as it already could in the form with one. Where a
punctuation replacement follows a renumbering in the same sentence it
binds to the renumbered unit: unlike a word operation it has no
distinguishing target text, so resolving it norm-wide would be useless.

A full acceptance test stays out of reach: hessenrecht.hessen.de is the
same login-only juris application as the Schleswig-Holstein and Berlin
portals. Checking landesrecht-bw.de too, that now covers all four
portals that could have supplied an annex-bearing statute, so the
LandesRechtTextParser's missing annex headings will need a stem from
recht.nrw.de instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Change-Id: Ief04164ee25653345a011f3ffbbfaa6df1f5c64b
diff --git a/README.adoc b/README.adoc
index 602f99d..77540fe 100644
--- a/README.adoc
+++ b/README.adoc
@@ -113,7 +113,10 @@
 -Einfügepaare, Einfügungen, deren Position ein Wortanker statt einer
 Stellenangabe bestimmt („Vor den Wörtern „Aus dem Bereich Verkehr:“ wird
 folgender Absatz 5 eingefügt“), Verbünde aus Umnummerierung und
-Folgeänderung („§ 50 wird zu § 38 und wird wie folgt geändert“),
+Folgeänderung („§ 50 wird zu § 38 und wird wie folgt geändert“, „Die
+bisherige Nr. 7 wird Nr. 5 und das Komma wird durch das Wort „und“
+ersetzt“ — eine Satzzeichen-Operation meint dabei stets die soeben
+umnummerierte Einheit, weil ihr Zieltext nichts unterscheidet),
 Verb-Rahmen, deren Unterpunkte allein die Fundstelle tragen („Es werden
 ersetzt: … in § 35 Absatz 3 die Angabe „X“ jeweils durch die Angabe
 „Y“,“) sowie die Neufassung der Gesetzesüberschrift.
@@ -167,7 +170,7 @@
 Ausführungsgesetz zum 17. Rundfunkänderungsstaatsvertrag und, mit 101
 Befehlen an 31 Normen der größte Landesrechtsfall, das WDR-Gesetz —
 mit Akzeptanztests gegen die
-amtlichen Nachfassungen; für Schleswig-Holstein und Berlin
+amtlichen Nachfassungen; für Schleswig-Holstein, Berlin und Hessen
 reicht die Prüfung bis zur Befehlserkennung — dort vollständig, aber ohne
 Anwendung —, weil deren Landesportale ihre Stammfassungen nur über eine
 anmeldepflichtige Schnittstelle ausgeben. Welche Konvention welches Land beisteuert, welche
diff --git a/src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java b/src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java
index f83ce2d..966ab5d 100644
--- a/src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java
+++ b/src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java
@@ -123,13 +123,15 @@
               + Z
               + " ersetzt\\.$");
 
+  // Wie ERSETZUNG, nur ohne Fundstelle — die liefert der Kontextrahmen. Das Objekt nach „durch“
+  // darf auch hier verkürzt sein („Die Angabe „X“ wird durch „Y“ ersetzt“, hessisches GVBl).
   private static final Pattern ERSETZUNG_OHNE_STELLE =
       Pattern.compile(
           "^(?:Die Wörter|Das Wort|Die Angabe|Die Zahl) "
               + Z
-              + " (?:wird|werden) (jeweils )?durch "
+              + " (?:wird|werden) (jeweils )?durch (?:"
               + WOERTER
-              + " "
+              + " )?"
               + Z
               + " ersetzt\\.$");
 
@@ -174,10 +176,13 @@
               + "\\.?$");
 
   // Ohne Stellenangabe: „Der Punkt am Ende wird durch die Angabe „…“ ersetzt.“ — das Satzzeichen ist
-  // hier selbst das Subjekt, die Fundstelle liefert der Kontextrahmen.
+  // hier selbst das Subjekt, die Fundstelle liefert der Kontextrahmen. Der Zusatz „am Ende“ darf
+  // fehlen („Das Komma wird durch das Wort „und“ ersetzt.“, hessisches GVBl): der bestimmte Artikel
+  // setzt dann voraus, dass die Einheit genau ein solches Satzzeichen trägt — was der Anwender
+  // prüft, statt es an das Einheitsende zu heften.
   private static final Pattern SATZZEICHEN_ERSETZUNG_OHNE_STELLE =
       Pattern.compile(
-          "^(Der Punkt|Das Komma|Das Semikolon) am Ende(?: des Satzes)? wird durch "
+          "^(Der Punkt|Das Komma|Das Semikolon)(?: am Ende(?: des Satzes)?)? wird durch "
               + "(ein Komma und "
               + WOERTER
               + " "
@@ -188,11 +193,12 @@
               + Z
               + ") ersetzt\\.$");
 
-  // Auch die Verbundform „wird der Punkt am Ende durch ein Komma und die Wörter „…“ ersetzt“.
+  // Auch die Verbundform „wird der Punkt am Ende durch ein Komma und die Wörter „…“ ersetzt“;
+  // „am Ende“ ist wie oben entbehrlich.
   private static final Pattern SATZZEICHEN_ERSETZUNG =
       Pattern.compile(
           "^(?:In )?(.+?) (?:wird|werden) (der Punkt|das Komma|das Semikolon)"
-              + " am Ende(?: des Satzes)? durch "
+              + "(?: am Ende(?: des Satzes)?)? durch "
               + "(ein Komma und "
               + WOERTER
               + " "
@@ -1014,7 +1020,7 @@
       } else {
         neu = satzzeichen(m.group(2));
       }
-      return Optional.of(new Ersetzung(kontext, alt, neu, false, true, provenienz));
+      return Optional.of(new Ersetzung(kontext, alt, neu, false, amEnde(text), provenienz));
     }
 
     if ((m = SATZZEICHEN_ERSETZUNG.matcher(text)).matches()) {
@@ -1029,8 +1035,9 @@
         neu = satzzeichen(m.group(3));
       }
       var neuText = neu;
+      var amEnde = amEnde(text);
       return StellenParser.parse(m.group(1))
-          .map(s -> new Ersetzung(kontext.plus(s), alt, neuText, false, true, provenienz));
+          .map(s -> new Ersetzung(kontext.plus(s), alt, neuText, false, amEnde, provenienz));
     }
 
     if ((m = WOERTER_EINFUEGUNG.matcher(text)).matches()) {
@@ -1519,6 +1526,10 @@
           "(?i)(?:die Angabe|nach der Angabe|vor der Angabe|die Wörter|die Worte|"
               + "nach den Wörtern|vor den Wörtern|das Wort)\\b");
 
+  // Klausel, deren Subjekt ein Satzzeichen ist („das Komma wird durch … ersetzt“).
+  private static final Pattern SATZZEICHEN_SUBJEKT =
+      Pattern.compile("(?i)(?:der Punkt|das Komma|das Semikolon)\\b");
+
   private static Optional<Aenderungsbefehl> erkenneVerbund(
       String text, Stelle kontext, ZitatExtraktor.Ergebnis zitate, Provenienz provenienz) {
     var sep = VERBUND_SEP.matcher(text);
@@ -1585,6 +1596,31 @@
         return imNeuen;
       }
     }
+    // „Die bisherige Nr. 7 wird Nr. 5 und das Komma wird durch das Wort „und“ ersetzt.“ — anders
+    // als eine Wortoperation trägt eine Satzzeichen-Operation keinen unterscheidenden Zieltext:
+    // norm-weit aufgelöst träfe „das Komma“ auf beliebig viele Vorkommen. Sie meint deshalb stets
+    // die soeben umnummerierte Einheit und wird auf diese festgelegt.
+    if (linksBefehl instanceof Umnummerierung umnummerierung
+        && SATZZEICHEN_SUBJEKT.matcher(rechts).lookingAt()) {
+      var relativ = relativeStelle(umnummerierung.neu(), kontext);
+      var vm = WIRD_WERDEN.matcher(rechts);
+      if (!relativ.istLeer() && vm.find()) {
+        // Grammatische Normalform wie beim Gapping: Lokativ voran, Verb vor das Subjekt.
+        var umgestellt =
+            "In "
+                + relativ.anzeigeText()
+                + " "
+                + vm.group().strip()
+                + " "
+                + rechts.substring(0, vm.start()).strip()
+                + " "
+                + rechts.substring(vm.end()).strip();
+        var imNeuen = erkenneAlsSatz(umgestellt, kontext, zitate, provenienz);
+        if (imNeuen.isPresent()) {
+          return imNeuen;
+        }
+      }
+    }
     // „Der bisherige Wortlaut wird Abs. 5 und in Halbsatz 1 wird … ersetzt“ (bayerisch): die
     // lokative Folgeklausel bezieht sich auf den soeben nummerierten Absatz.
     if (linksBefehl instanceof WortlautZuAbsatz wz
@@ -2074,6 +2110,17 @@
     return zitat(zitate, index).replaceAll("\\s+", " ").strip();
   }
 
+  /**
+   * Nennt der Befehl das Satzzeichen ausdrücklich „am Ende“, so ist es dort zu ersetzen; sonst
+   * verlangt der bestimmte Artikel, dass die Einheit genau eines trägt — das prüft der Anwender.
+   * Zitate sind hier bereits maskiert, die Wendung kann also nicht aus dem Ersatztext stammen.
+   */
+  private static boolean amEnde(String text) {
+    return AM_ENDE.matcher(text).find();
+  }
+
+  private static final Pattern AM_ENDE = Pattern.compile("\\bam Ende\\b");
+
   private static String satzzeichen(String phrase) {
     // Am Satzanfang steht dieselbe Phrase großgeschrieben („Der Punkt am Ende wird …“).
     return switch (Character.toLowerCase(phrase.charAt(0)) + phrase.substring(1)) {
diff --git a/src/main/java/eu/mulk/aendggner/aenderung/parse/TextBereiniger.java b/src/main/java/eu/mulk/aendggner/aenderung/parse/TextBereiniger.java
index 3945df9..483debc 100644
--- a/src/main/java/eu/mulk/aendggner/aenderung/parse/TextBereiniger.java
+++ b/src/main/java/eu/mulk/aendggner/aenderung/parse/TextBereiniger.java
@@ -146,6 +146,26 @@
       Pattern.compile(
           "^\\s*(?:Nummer \\d{4}/\\d{1,3}|\\d{4}/\\d{1,3} vom \\d{1,2}\\. \\p{L}+)\\s*$");
 
+  // GVBl. für das Land Hessen: laufende Fußzeile („Gesetz- und Verordnungsblatt für das Land
+  // Hessen ­ Nr. 5 vom 3. Februar 2026 2“) und der Rest des Titelkopfs („für das Land Hessen“,
+  // „2026 Wiesbaden, den 3. Februar 2026 Nr. 5“). Die Fußzeile steht zwischen zwei
+  // Gliederungspunkten und hängte sich sonst an den vorangehenden Befehl. Der Trenner ist ein
+  // weiches Trennzeichen (U+00AD), kein Gedankenstrich.
+  private static final Pattern GVBL_HESSEN_FUSS =
+      Pattern.compile(
+          "^\\s*Gesetz- und Verordnungsblatt für das Land Hessen\\s*[\\u00ad–-]\\s*Nr\\. \\d+"
+              + " vom .+? \\d{4}\\s+\\d{1,4}\\s*$");
+  private static final Pattern GVBL_HESSEN_KOPF =
+      Pattern.compile(
+          "^\\s*(?:für das Land Hessen|\\d{4}\\s+\\p{Lu}\\p{L}+,\\s+den .+? \\d{4}\\s+Nr\\. \\d+)"
+              + "\\s*$");
+
+  // Amtliche Sternchen-Fußnote der Verordnungs-/Gesetzesüberschrift, die auf die Fundstellennummer
+  // des geänderten Rechts verweist („* Ändert FFN 61-60“, Hessen; „* Ändert Gl.Nr. …“). Sie steht
+  // am Seitenfuß mitten zwischen zwei Gliederungspunkten und ist damit funktional Seitenmöbel.
+  private static final Pattern FUNDSTELLEN_FUSSNOTE =
+      Pattern.compile("^\\s*\\*\\s*Ändert (?:FFN|Gl\\.\\s?Nr\\.|GVBl\\.).*$");
+
   /** Konjunktionen, die typischerweise auf einen Suspensivstrich folgen („Wirk- und …“). */
   private static final Pattern KONJUNKTION =
       Pattern.compile("^(und|oder|sowie|bzw\\.|beziehungsweise)\\b.*");
@@ -372,7 +392,10 @@
         || NDS_HERAUSGEBER.matcher(zeile).matches()
         || GVOBL_SH_KOPF.matcher(zeile).matches()
         || GVOBL_SH_LAND.matcher(zeile).matches()
-        || GVOBL_SH_HEFT.matcher(zeile).matches();
+        || GVOBL_SH_HEFT.matcher(zeile).matches()
+        || GVBL_HESSEN_FUSS.matcher(zeile).matches()
+        || GVBL_HESSEN_KOPF.matcher(zeile).matches()
+        || FUNDSTELLEN_FUSSNOTE.matcher(zeile).matches();
   }
 
   /**
diff --git a/src/main/resources/sampledata/Landesrecht-Beispiele.adoc b/src/main/resources/sampledata/Landesrecht-Beispiele.adoc
index 45c4e4f..be70dfc 100644
--- a/src/main/resources/sampledata/Landesrecht-Beispiele.adoc
+++ b/src/main/resources/sampledata/Landesrecht-Beispiele.adoc
@@ -44,7 +44,9 @@
 03.02.2026 (Elfte Verordnung zur Änderung der Verordnung zur Bestimmung verkehrsrechtlicher
 Zuständigkeiten)
 |*Sperrsatz* in Datums- und Signaturzeilen („3 . F e bru a r 2 02 6", „Der Mi n is t er pr
-äs i d e nt") als Extraktions-Härtetest; *abgekürztes „Abs."/„Nr." im §-Kontext*; FFN-Fußnote.
+äs i d e nt") — er trifft, anders als erwartet, nur den Unterschriftenblock und keinen Befehl;
+*abgekürztes „Abs."/„Nr." im §-Kontext*; die *FFN-Fußnote der Überschrift* („* Ändert FFN 61-60")
+steht am Seitenfuß mitten zwischen zwei Gliederungspunkten.
 
 |Niedersachsen
 |`Niedersachsen/Nds-GVBl-2026-10_ELER-Foerdergesetz-AendG.pdf` — Nds. GVBl. 2026 Nr. 10 vom
@@ -133,6 +135,13 @@
   Aufzählungspunkt *vollständig* erkannt — alle sechs Befehle, einschließlich der Änderungen an der
   unnummerierten Anlage. Ein voller Akzeptanztest bleibt an der Stammfassung hängen (siehe „Noch
   offen“).
+* *Hessen* ist bis zur Befehlserkennung umgesetzt (`EndToEndTest.verkehrsZustVAendVOHessen`): alle
+  21 Befehle des Artikels 1 werden erkannt. Der als Härtetest erwartete Sperrsatz erwies sich als
+  harmlos — er trifft nur den Unterschriftenblock. Zu tun war dreierlei: die laufende GVBl-Fußzeile
+  und der Masthead als Kolumnentitel, die Sternchen-Fußnote der Überschrift („* Ändert FFN 61-60“,
+  am Seitenfuß zwischen zwei Gliederungspunkten) ebenso, und zwei Befehlsformen — der Zusatz
+  „am Ende“ darf beim Satzzeichen fehlen, und das Objekt hinter „durch“ darf auch ohne Fundstelle
+  verkürzt sein. Ein voller Akzeptanztest ist *nicht* möglich — siehe „Noch offen“.
 * Baden-Württemberg (GBl 2026 Nr. 26): großes Anlage-lastiges Änderungsgesetz (§§ 4–57a + Anlagen
   1/3b/4b); Stammfassung/Akzeptanztest noch offen.
 
@@ -154,12 +163,14 @@
 per `…id_/…`-Rohform laden. Achtung, `WebFetch` erreicht `web.archive.org` nicht — nur `curl`.
 
 juris-Landesportale:: Sackgasse — *geprüft* für `gesetze-rechtsprechung.sh.juris.de`
-(Schleswig-Holstein) und `gesetze.berlin.de` (Berlin, trotz Eigennamen dieselbe Anwendung). Beide
-sind seit etwa Ende 2022 reine Single-Page-Anwendungen; sämtliche Dokument-URLs liefern nur die
-leere Anwendungshülle, die Inhalte hängen an einer tokenpflichtigen Schnittstelle. Auch archivierte
+(Schleswig-Holstein), `gesetze.berlin.de` (Berlin, trotz Eigennamen dieselbe Anwendung),
+`landesrecht-bw.de` (Baden-Württemberg) und `rv.hessenrecht.hessen.de` (Hessen, Kennung `bshe`).
+Alle vier sind seit etwa Ende 2022 reine Single-Page-Anwendungen; sämtliche Dokument-URLs liefern
+nur die leere Anwendungshülle (rund 5,4 kB), die Inhalte hängen an einer tokenpflichtigen
+Schnittstelle. Auch archivierte
 Einzelnorm-Seiten sind bereits Hüllen. Brauchbar sind allenfalls Wayback-Snapshots aus der Zeit
 *vor* der Umstellung — die sind aber meist zu alt für die gesuchte Fassung. Für
-`landesrecht-bw.de`, `hessenrecht.hessen.de`, `landesrecht.thueringen.de` und `voris.niedersachsen.de`
+`landesrecht.thueringen.de` und `voris.niedersachsen.de`
 ist dasselbe zu erwarten, aber *nicht* nachgeprüft: Ein Versuch lohnt, bevor man aufgibt.
 
 Fällt das Portal aus, bleibt der Weg über das Gesetzblatt selbst: Trägt der Einleitungssatz des
@@ -189,12 +200,16 @@
   juris-Anwendung wie das schleswig-holsteinische Portal (Kennung `bsbe`) und ebenso
   anmeldepflichtig. In der Wayback Machine gibt es keine serverseitig gerenderten Volltexte des
   ASOG oder des LAF-Errichtungsgesetzes.
+* *Hessen — Stammfassung nicht beschaffbar*: `rv.hessenrecht.hessen.de` ist dieselbe juris-Anwendung
+  (Kennung `bshe`) wie die Portale Schleswig-Holsteins und Berlins; Dokument-URLs liefern nur die
+  Anwendungshülle. Die Befehlserkennung ist vollständig belegt, die Anwendung bleibt offen.
 * *Anlagen im kanonischen Klartext*: Anlagen sind als Änderungsziel unterstützt — im gii-XML sind
   sie eigene Normen („Anlage 8“, „Anhang“), und `Stelle.anlagenEnbez()` löst sie auf; im GEG werden
   anlagenbezogene Befehle angewandt. Der `LandesRechtTextParser` kennt jedoch nur „§“- und
   „Art.“-Normköpfe: Eine handgepflegte Stammfassung kann *keine* Anlage tragen. Das ist die
-  konkrete Lücke für Berlin (Artikel 1) und Baden-Württemberg (Anlagen 1/3b/4b) — sie wird erst
-  behebbar, wenn für eines der beiden eine Stammfassung vorliegt.
+  konkrete Lücke für Berlin (Artikel 1) und Baden-Württemberg (Anlagen 1/3b/4b). Beide Portale sind
+  inzwischen als juris-Anwendung *nachgeprüft* — die Lücke geht deshalb erst mit einem
+  Anlage-tragenden Stammgesetz aus einem anderen Portal auf, praktisch `recht.nrw.de`.
 * *Berlin, ganzseitenbreite Rahmen*: Titelblock und Impressum stehen im Inhaltsstrom an anderer
   Stelle als im Satzbild (der Titelblock mitten im Fließtext der ersten Seite). Das zu beheben
   verlangt eine geometrische Lesereihenfolge (XY-Cut), nicht bloß eine Spaltenerkennung.
@@ -208,15 +223,20 @@
 Kein Land scheitert noch an reiner Extraktionsschwäche; offen sind Modell- und Beschaffungsfragen.
 Nach absteigendem Nutzen:
 
-. *Anlagen-Normköpfe im `LandesRechtTextParser`* — der Parser kennt jetzt neben „§“/„Art.“ auch
-  die Inhaltsübersicht als Norm; die Anlage fehlt noch. Lohnt, sobald für Berlin bzw.
-  Baden-Württemberg eine Stammfassung vorliegt, sonst nicht end-to-end prüfbar.
-. *Hessen* als Sperrsatz-Härtetest („3 . F e bru a r 2 02 6“). Betrifft nur Datums- und
-  Signaturzeilen, nicht die Befehle — kosmetisch, aber ein eigener Extraktionsfall.
+. *Ein Anlage-tragendes Stammgesetz über `recht.nrw.de` beschaffen* — und dann die
+  *Anlagen-Normköpfe im `LandesRechtTextParser`* ergänzen (er kennt neben „§“/„Art.“ inzwischen
+  auch die Inhaltsübersicht als Norm). Über Berlin oder Baden-Württemberg geht das nicht mehr:
+  deren Portale sind als juris-Anwendung nachgeprüft. Ohne eine solche Stammfassung bleibt die
+  Ergänzung nicht end-to-end prüfbar.
+. *Thüringen* (`Thueringen/GVBl-TH-2026-02_…`) ist als einziges beschafftes Heft noch gar nicht
+  angefasst — ein Sammelheft mit mehreren Verkündungen, also zugleich ein Test der Artikel-/
+  Verkündungsauswahl.
+. *Berlin, ganzseitenbreite Rahmen* (XY-Cut) — die letzte offene Layout-Frage, siehe „Noch offen“.
 
 Regressionsschutz: `./mvnw verify` prüft die bestehenden Belegfälle mit. Gepinnt ist dabei nicht
 überall dieselbe Art von Zahl — für UWG, AGG und ProdHaftG, dass *kein* Befehl manuell bleibt; für
 das BayJG die 154 erkannten Befehle und namentlich die drei Umnummerierungs-Residuen; für Sachsen,
-Niedersachsen und die drei NRW-Artikel die vollständige Anwendung bis auf das eine benannte
-Residuum im WDR-Gesetz. Wer eine dieser Zahlen
+Niedersachsen und die vier NRW-Artikel die vollständige Anwendung bis auf das eine benannte
+Residuum im WDR-Gesetz; für Schleswig-Holstein, Berlin und Hessen die vollständige *Erkennung*
+(dort 21 Befehle). Wer eine dieser Zahlen
 ändern muss, sollte den Grund im Test vermerken.
diff --git a/src/test/java/eu/mulk/aendggner/EndToEndTest.java b/src/test/java/eu/mulk/aendggner/EndToEndTest.java
index 035c3f1..4e4d547 100644
--- a/src/test/java/eu/mulk/aendggner/EndToEndTest.java
+++ b/src/test/java/eu/mulk/aendggner/EndToEndTest.java
@@ -844,4 +844,87 @@
     assertThat(neu.norm("§ 3").orElseThrow().gesamtText())
         .isEqualTo(gesetz.norm("§ 3").orElseThrow().gesamtText());
   }
+
+  /**
+   * Hessen: Die Elfte Verordnung zur Änderung der Verordnung zur Bestimmung verkehrsrechtlicher
+   * Zuständigkeiten (GVBl. 2026 Nr. 5). Der Test hält Extraktion und Befehlserkennung fest — alle
+   * 21 Befehle des Artikels 1 werden erkannt.
+   *
+   * <p>Kein voller Akzeptanztest: {@code hessenrecht.hessen.de} ist dieselbe anmeldepflichtige
+   * juris-Anwendung wie die Portale Schleswig-Holsteins und Berlins, die Stammfassung ist daraus
+   * nicht zu beschaffen.
+   *
+   * <p>Der hessische Sperrsatz („Der Mi n is t er“) bleibt bewusst unbehandelt: Er trifft nur den
+   * Unterschriftenblock am Dokumentende, keinen einzigen Befehl.
+   */
+  @Test
+  void verkehrsZustVAendVOHessen() throws Exception {
+    var pdf =
+        SAMPLEDATA.resolve("Hessen/GVBl-2026-05_11-AendVO-verkehrsrechtl-Zustaendigkeiten.pdf");
+    assumeTrue(Files.exists(pdf), "GVBl-Hessen-Beispiel-PDF fehlt");
+
+    var text = TextBereiniger.bereinige(new PatchTextExtraktor().extrahiere(pdf));
+
+    // Die laufende Fußzeile stand zwischen den Punkten 11. und 12. und hing sonst an Punkt 12.
+    assertThat(text).doesNotContain("Gesetz- und Verordnungsblatt für das Land Hessen");
+    // Die amtliche Sternchen-Fußnote der Überschrift steht am Seitenfuß zwischen 4. b) und 5. und
+    // hing sonst an Punkt 4. b).
+    assertThat(text).doesNotContain("Ändert FFN");
+    // Der Sperrsatz beschränkt sich auf den Unterschriftenblock — hier bewusst nicht geheilt.
+    assertThat(text).contains("Min isterpräsident");
+
+    var verordnung =
+        new Gesetz(
+            "VerkZustV",
+            "Verordnung zur Bestimmung verkehrsrechtlicher Zuständigkeiten",
+            null,
+            List.of());
+    var ergebnis = new AenderungsgesetzParser().parse(text, verordnung, null);
+    // Artikel 2 regelt nur das Inkrafttreten.
+    assertThat(ergebnis.artikel()).containsExactly("1");
+    assertThat(ergebnis.befehle()).hasSize(21);
+    assertThat(ergebnis.befehle()).noneMatch(b -> b instanceof UnbekannterBefehl);
+
+    // Hessen kürzt im §-Kontext durchweg ab („In Abs. 1 …“, „Nr. 1 wird aufgehoben“); der Rahmen
+    // liefert den Paragraphen.
+    assertThat(befehlZu(ergebnis, "1. a)").stelle().anzeigeText()).isEqualTo("§ 6 Absatz 1");
+    assertThat(befehlZu(ergebnis, "5. a)").stelle().anzeigeText()).isEqualTo("§ 14 Nummer 1");
+    // „Die Absatzbezeichnung „(1)“ wird gestrichen.“ — die Bezeichnung selbst ist das Ziel.
+    assertThat(befehlZu(ergebnis, "4. a)").stelle().anzeigeText())
+        .isEqualTo("§ 13 Absatzbezeichnung (1)");
+
+    // 5. b) und 5. f) sind Verbünde aus Umnummerierung und einer Folgeoperation ohne eigenen
+    // Lokativ. Die Wortoperation löst norm-weit auf (ihr Zieltext ist unterscheidend), die
+    // Satzzeichen-Operation dagegen wird auf die umnummerierte Nummer festgelegt — „das Komma“
+    // träfe sonst beliebig viele Vorkommen.
+    var umnummerierungUndErsetzung = (Aenderungsbefehl.Sammelbefehl) befehlZu(ergebnis, "5. b)");
+    assertThat(umnummerierungUndErsetzung.teilbefehle())
+        .extracting(b -> b.stelle().anzeigeText())
+        .containsExactly("§ 14 Nummer 2", "§ 14");
+    var umnummerierungUndKomma = (Aenderungsbefehl.Sammelbefehl) befehlZu(ergebnis, "5. f)");
+    assertThat(umnummerierungUndKomma.teilbefehle())
+        .extracting(b -> b.stelle().anzeigeText())
+        .containsExactly("§ 14 Nummer 7", "§ 14 Nummer 5");
+    var komma = (Aenderungsbefehl.Ersetzung) umnummerierungUndKomma.teilbefehle().get(1);
+    assertThat(komma.alt()).isEqualTo(",");
+    assertThat(komma.neu()).isEqualTo("und");
+    // Ohne den Zusatz „am Ende“ ist das Satzzeichen nicht ans Einheitsende geheftet; der Anwender
+    // verlangt dann, dass die Einheit genau eines trägt.
+    assertThat(komma.amEnde()).isFalse();
+
+    // Bereichs-Umnummerierung, absteigend angewandt: „Die bisherigen Nr. 3 und 4 werden die
+    // Nr. 2 und 3.“
+    var bereich = (Aenderungsbefehl.Sammelbefehl) befehlZu(ergebnis, "5. c)");
+    assertThat(bereich.teilbefehle())
+        .extracting(b -> b.stelle().anzeigeText())
+        .containsExactly("§ 14 Nummer 4", "§ 14 Nummer 3");
+  }
+
+  private static Aenderungsbefehl befehlZu(
+      AenderungsgesetzParser.ParseErgebnis ergebnis, String gliederungsPfad) {
+    return ergebnis.befehle().stream()
+        .filter(b -> b.provenienz().gliederungsPfad().equals(gliederungsPfad))
+        .findFirst()
+        .orElseThrow();
+  }
 }
diff --git a/src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java b/src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java
index 8fcf3e5..46c5ad6 100644
--- a/src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java
+++ b/src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java
@@ -78,6 +78,62 @@
     assertThat(ersetzung.amEnde()).isTrue();
   }
 
+  /**
+   * Hessisches GVBl: Der Zusatz „am Ende“ darf fehlen. Der bestimmte Artikel setzt dann voraus,
+   * dass die Einheit genau ein solches Satzzeichen trägt — das prüft der Anwender, statt dass die
+   * Erkennung es ans Einheitsende heftet.
+   */
+  @Test
+  void erkenntSatzzeichenErsetzungOhneEndeAngabe() {
+    var befehl =
+        erkenne(
+            "In Nummer 5 wird das Komma durch das Wort „und“ ersetzt.",
+            new Stelle(List.of(new Stelle.Paragraph("14"))));
+
+    assertThat(befehl).containsInstanceOf(Ersetzung.class);
+    var ersetzung = (Ersetzung) befehl.orElseThrow();
+    assertThat(ersetzung.stelle().anzeigeText()).isEqualTo("§ 14 Nummer 5");
+    assertThat(ersetzung.alt()).isEqualTo(",");
+    assertThat(ersetzung.neu()).isEqualTo("und");
+    assertThat(ersetzung.amEnde()).isFalse();
+  }
+
+  /** Ohne Fundstelle und mit verkürztem Objekt hinter „durch“ (hessisches GVBl). */
+  @Test
+  void erkenntErsetzungOhneStelleMitVerkuerztemObjekt() {
+    var befehl =
+        erkenne(
+            "Die Angabe „(§ 19 Abs. 3 Satz 1 Nr. 3 oder 4)“ wird durch „(§ 19 Abs. 3 Satz 1"
+                + " Nr. 3)“ ersetzt.",
+            new Stelle(List.of(new Stelle.Paragraph("14"))));
+
+    assertThat(befehl).containsInstanceOf(Ersetzung.class);
+    var ersetzung = (Ersetzung) befehl.orElseThrow();
+    assertThat(ersetzung.stelle().anzeigeText()).isEqualTo("§ 14");
+    assertThat(ersetzung.alt()).isEqualTo("(§ 19 Abs. 3 Satz 1 Nr. 3 oder 4)");
+    assertThat(ersetzung.neu()).isEqualTo("(§ 19 Abs. 3 Satz 1 Nr. 3)");
+  }
+
+  /**
+   * Verbund aus Umnummerierung und Satzzeichen-Operation: Anders als eine Wortoperation trägt sie
+   * keinen unterscheidenden Zieltext und meint deshalb die soeben umnummerierte Einheit.
+   */
+  @Test
+  void bindetSatzzeichenKlauselAnDieUmnummerierteEinheit() {
+    var befehl =
+        erkenne(
+            "Die bisherige Nr. 7 wird Nr. 5 und das Komma wird durch das Wort „und“ ersetzt.",
+            new Stelle(List.of(new Stelle.Paragraph("14"))));
+
+    assertThat(befehl).containsInstanceOf(Sammelbefehl.class);
+    var teile = ((Sammelbefehl) befehl.orElseThrow()).teilbefehle();
+    assertThat(teile).hasSize(2);
+    assertThat(teile.get(0)).isInstanceOf(Umnummerierung.class);
+    assertThat(teile.get(0).stelle().anzeigeText()).isEqualTo("§ 14 Nummer 7");
+    assertThat(teile.get(1).stelle().anzeigeText()).isEqualTo("§ 14 Nummer 5");
+    assertThat(((Ersetzung) teile.get(1)).neu()).isEqualTo("und");
+  }
+
   @Test
   void erkenntNeufassung() {
     var befehl =
diff --git a/src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java b/src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java
index 284c506..1591a74 100644
--- a/src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java
+++ b/src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java
@@ -289,6 +289,22 @@
   }
 
   @Test
+  void entferntSeitenmoebelDesGvblHessen() {
+    // Der Trenner der Fußzeile ist ein weiches Trennzeichen (U+00AD), kein Gedankenstrich; die
+    // Sternchen-Fußnote der Überschrift steht am Seitenfuß mitten zwischen zwei Punkten.
+    var roh =
+        """
+        für das Land Hessen
+        2026 Wiesbaden,  den 3. Februar 2026 Nr. 5
+        Erster Satz.
+        * Ändert FFN 61-60
+        Gesetz- und Verordnungsblatt für das Land Hessen ­ Nr. 5 vom 3. Februar 2026 2
+        Zweiter Satz.""";
+
+    assertThat(TextBereiniger.bereinige(roh)).isEqualTo("Erster Satz.\nZweiter Satz.");
+  }
+
+  @Test
   void repariertInvertierteZitatzeichenAnAbsatzmarkern() {
     // BMJV-Vorlagen zeichnen das hängende „ nach dem Absatzmarker bzw. der Paragraphenangabe.
     assertThat(TextBereiniger.bereinige("(1) „ Ungeachtet des § 8 gilt.“"))