Add Schleswig-Holstein extraction case and three cleanup fixes

The GVOBl. Schl.-H. amendment act (Kommunalrecht, 2026/27) brings three
conventions the tool had not seen:

  * a running two-line page header that sits between articles and glued
    itself onto the following article heading,
  * superscript footnote markers on article headings ("Artikel 1 ¹)"),
    which defeated the article split, and
  * ragged-right typesetting, where the geometric margin detection finds
    no alignment cluster and reports full lines as hard line ends, so a
    trailing hyphen was never joined.

All three are fixed in TextBereiniger; the hyphen rule now trusts the
extractor's trailing-space signal over the geometric class.

A full acceptance test is not possible: the Schleswig-Holstein law portal
serves document bodies only through an authenticated API, and the last
freely archived consolidated Gemeindeordnung (14 Nov 2022) predates the
amended § 34a. The new test therefore covers everything up to and
including command recognition; the blocker is recorded in
Landesrecht-Beispiele.adoc.

219 tests green; Bund/Bayern/Sachsen/Niedersachsen figures unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Change-Id: I9b3ab8a756372504b51c4d410b491fded255da96
diff --git a/FASSUNGEN.txt b/FASSUNGEN.txt
index d3605b2..e09cc8a 100644
--- a/FASSUNGEN.txt
+++ b/FASSUNGEN.txt
@@ -21,6 +21,72 @@
 
 
 ════════════════════════════════════════════════════════════════════════════════
+ Fassung vom 25. Juli 2026,
+ zuletzt geändert durch die am 25. Juli 2026 vorgenommenen Änderungen
+════════════════════════════════════════════════════════════════════════════════
+
+Die sechste Ertüchtigungswelle wird um den schleswig-holsteinischen Belegfall
+ergänzt: das Gesetz zur Änderung des Gesetzes zur Änderung kommunalrechtlicher
+Vorschriften vom 26. März 2026 (GVOBl. Schl.-H. 2026/27). Es führt drei bislang
+unbekannte Eigenheiten ein — den laufenden Seitenkopf des GVOBl. Schleswig-
+Holstein, hochgestellte Fußnotenmarker an den Artikel-Überschriften und einen
+Flattersatz, in dem die geometrische Randerkennung versagt. Ein voller
+Akzeptanztest bleibt vorerst aus, weil die konsolidierte Gemeindeordnung aus
+freien Quellen nicht in der maßgeblichen Fassung zu beschaffen ist.
+
+
+Artikel 1
+Seitenkopf des Gesetz- und Verordnungsblatts für Schleswig-Holstein
+
+Der Textbereiniger (TextBereiniger.istKolumnentitel) entfernt nunmehr auch den
+zweizeiligen Titelkopf und den laufenden Seitenkopf des Gesetz- und Verordnungs-
+blatts für Schleswig-Holstein („Gesetz- und Verordnungsblatt für Schleswig-
+Holstein“ nebst „2026/27 vom 30. März“, Muster GVOBL_SH_KOPF, GVOBL_SH_LAND und
+GVOBL_SH_HEFT). Der laufende Kopf steht zwischen zwei Artikeln und klebte sonst
+an der folgenden Artikel-Überschrift.
+
+
+Artikel 2
+Fußnotenmarker an Artikel-Überschriften
+
+Schleswig-Holstein verweist an der Artikel-Überschrift eines Änderungsgesetzes
+mit einer hochgestellten Ziffer auf die Gliederungsnummer-Fußnote am Seitenfuß
+(„Artikel 1 ¹)“). Solche Überschriften genügten dem Muster der Artikel-Teilung
+(AenderungsgesetzParser.ARTIKEL_UEBERSCHRIFT) nicht. Der Textbereiniger streift
+den Marker nunmehr ab (TextBereiniger.loeseArtikelFussnoten, Muster
+ARTIKEL_FUSSNOTE) und trennt dabei eine in derselben Zeile stehende Einleitung
+wieder ab.
+
+
+Artikel 3
+Silbentrennung bei hartem Zeilenende
+
+Der Textbereiniger (TextBereiniger.verbindeUmbrueche) zog eine Silbentrennung am
+Zeilenende bislang nie zusammen, wenn die geometrische Randerkennung des
+Fontgrößenfilters ein hartes Zeilenende meldete. Im Flattersatz — wie ihn das
+GVOBl. Schl.-H. verwendet — findet diese Erkennung jedoch keinen Ausrichtungs-
+Cluster und stuft auch volle Zeilen als hart ein. Klebt der Trennstrich ohne
+Trailing-Whitespace am Zeilenende, ist er deshalb nunmehr auch dort als
+Silbentrennung zu lesen; bewusst gesetzte Suspensivstriche tragen den Trailing-
+Whitespace des Extraktors und bleiben unberührt.
+
+
+Artikel 4
+Belegfall Kommunalrecht-Änderungsgesetz Schleswig-Holstein
+
+Als weiterer Prüffall tritt ein Test bis einschließlich der Befehlserkennung
+hinzu (EndToEndTest.kommunalrechtAendGSchleswigHolstein): Artikel 1 trifft die
+Gemeindeordnung, Artikel 2 die Kreisordnung, Artikel 3 ein anderes Änderungs-
+gesetz; je Zielgesetz wird genau ein Neufassungsbefehl („§ 34a Absatz 1 erhält
+folgende Fassung“, „§ 29a Absatz 1 …“) erkannt, kein Befehl bleibt unbekannt.
+Ein voller Akzeptanztest ist nicht möglich: Das Landesportal
+gesetze-rechtsprechung.sh.juris.de gibt seine Dokumentinhalte nur über eine
+anmeldepflichtige Schnittstelle aus, und die letzte frei archivierte Gesamt-
+ausgabe der Gemeindeordnung (14. November 2022) kennt den geänderten § 34a noch
+nicht. Die Fundstelle ist in sampledata/Landesrecht-Beispiele.adoc vermerkt.
+
+
+════════════════════════════════════════════════════════════════════════════════
  Fassung vom 24. Juli 2026,
  zuletzt geändert durch die am 24. Juli 2026 vorgenommenen Änderungen
 ════════════════════════════════════════════════════════════════════════════════
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 9c1edaa..7460b15 100644
--- a/src/main/java/eu/mulk/aendggner/aenderung/parse/TextBereiniger.java
+++ b/src/main/java/eu/mulk/aendggner/aenderung/parse/TextBereiniger.java
@@ -1,5 +1,6 @@
 package eu.mulk.aendggner.aenderung.parse;
 
+import eu.mulk.aendggner.gesetz.Superskript;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.regex.Pattern;
@@ -131,6 +132,19 @@
   private static final Pattern NDS_HERAUSGEBER =
       Pattern.compile("^\\s*Herausgeber: Niedersächsische Staatskanzlei\\s*$");
 
+  // GVOBl. Schleswig-Holstein: zweizeiliger Titelkopf („Gesetz- und Verordnungsblatt“ / „für
+  // Schleswig-Holstein“, auf Folgeseiten in einer Zeile) und die Heftangabe („Nummer 2026/27“ auf
+  // dem Titelblatt, „2026/27 vom 30. März“ als zweite Zeile des laufenden Seitenkopfs). Der
+  // laufende Kopf steht mitten im Text zwischen zwei Artikeln und klebte sonst an der folgenden
+  // Artikel-Überschrift.
+  private static final Pattern GVOBL_SH_KOPF =
+      Pattern.compile("^\\s*Gesetz- und Verordnungsblatt(?: für Schleswig-Holstein)?\\s*$");
+  private static final Pattern GVOBL_SH_LAND =
+      Pattern.compile("^\\s*für Schleswig-Holstein\\s*$");
+  private static final Pattern GVOBL_SH_HEFT =
+      Pattern.compile(
+          "^\\s*(?:Nummer \\d{4}/\\d{1,3}|\\d{4}/\\d{1,3} vom \\d{1,2}\\. \\p{L}+)\\s*$");
+
   /** Konjunktionen, die typischerweise auf einen Suspensivstrich folgen („Wirk- und …“). */
   private static final Pattern KONJUNKTION =
       Pattern.compile("^(und|oder|sowie|bzw\\.|beziehungsweise)\\b.*");
@@ -192,7 +206,7 @@
     var verbunden = verbindeUmbrueche(zeilen);
     // Falsch-positive markerlose Zusammenzüge („durch“ + „die“ → „durchdie“) reparieren — die
     // Befehlsvokabeln sind nie Kompositum-Bestandteile.
-    return trenneVerklebteZitatgrenzen(reflowUndStrippe(verbunden));
+    return loeseArtikelFussnoten(trenneVerklebteZitatgrenzen(reflowUndStrippe(verbunden)));
   }
 
   /**
@@ -262,6 +276,20 @@
         .replaceAll("(wie folgt geändert:?)(?=[a-z]{1,3}\\)|\\d+[a-z]?\\.)", "$1\n");
   }
 
+  /**
+   * Streift hochgestellte Fußnotenmarker von Artikel-Überschriften ab („Artikel 1 ¹)“ → „Artikel
+   * 1“). Schleswig-Holstein verweist damit auf die Gl.-Nr.-Fußnoten am Seitenfuß. Der Marker
+   * verhindert sonst die Artikel-Teilung; steht die Einleitung in derselben Zeile, wird sie dabei
+   * wieder abgetrennt.
+   */
+  private static String loeseArtikelFussnoten(String text) {
+    return ARTIKEL_FUSSNOTE.matcher(text).replaceAll("$1\n");
+  }
+
+  private static final Pattern ARTIKEL_FUSSNOTE =
+      Pattern.compile(
+          "(?m)^(Artikel\\s+\\d+[a-z]?)\\s*" + Superskript.LAUF.pattern() + "\\)[ \\t]*\\n?");
+
   /** Entfernt Seitenkopf-/Fußzeilen. Trailing-Whitespace der übrigen Zeilen bleibt erhalten! */
   private static ArrayList<Zeile> entferneKolumnentitel(List<Zeile> roh) {
     var kolumnentitel = new boolean[roh.size()];
@@ -302,7 +330,10 @@
         || LANDTAG_MARKE.matcher(zeile).matches()
         || REVOSAX_FUSS.matcher(zeile).matches()
         || NDS_GVBL_FUSS.matcher(zeile).matches()
-        || NDS_HERAUSGEBER.matcher(zeile).matches();
+        || NDS_HERAUSGEBER.matcher(zeile).matches()
+        || GVOBL_SH_KOPF.matcher(zeile).matches()
+        || GVOBL_SH_LAND.matcher(zeile).matches()
+        || GVOBL_SH_HEFT.matcher(zeile).matches();
   }
 
   /**
@@ -342,7 +373,13 @@
       while (true) {
         var gestutzt = zeile.stripTrailing();
         // Ein geometrisch hartes Zeilenende ist ein bewusster Umbruch — nie zusammenziehen.
-        var mitTrennstrich = umbruch != Umbruch.HART && endetMitSilbentrennung(gestutzt);
+        // Ausnahme: Klebt der Trennstrich ohne Trailing-Whitespace am Zeilenende, ist er eine
+        // echte Silbentrennung. Im Flattersatz (GVOBl. Schl.-H.) findet die Randerkennung keinen
+        // Ausrichtungs-Cluster und stuft auch volle Zeilen als hart ein; das Trailing-Space-Signal
+        // des Extraktors ist dort das verlässlichere Kriterium.
+        var mitTrennstrich =
+            endetMitSilbentrennung(gestutzt)
+                && (umbruch != Umbruch.HART || gestutzt.length() == zeile.length());
         var markerlos =
             markerlosAktiv
                 && endetMarkerlos(zeile)
diff --git a/src/main/resources/sampledata/Landesrecht-Beispiele.adoc b/src/main/resources/sampledata/Landesrecht-Beispiele.adoc
index f358ae4..373f615 100644
--- a/src/main/resources/sampledata/Landesrecht-Beispiele.adoc
+++ b/src/main/resources/sampledata/Landesrecht-Beispiele.adoc
@@ -88,12 +88,24 @@
   dem zweispaltigen Nds. GVBl 2022 Nr. 33) und `EndToEndTest.nefgAcceptance` (fünf von sieben
   Befehlen angewandt; zwei Einfügeformen — Sachnummer mit Leerzeichen „§ 2 a“, kapitelbezogene
   §-Block-Einfügung — noch nicht unterstützt, als Grenze dokumentiert).
+* *Schleswig-Holstein* ist bis einschließlich der Befehlserkennung umgesetzt
+  (`EndToEndTest.kommunalrechtAendGSchleswigHolstein`): Artikel 1 trifft die Gemeindeordnung,
+  Artikel 2 die Kreisordnung, je ein Neufassungsbefehl, kein unbekannter Befehl. 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.
-* Für Schleswig-Holstein fehlt die konsolidierte Stammfassung noch (siehe oben).
 
 == Noch offen
 
+* *Schleswig-Holstein — Stammfassung nicht beschaffbar*: Das Landesportal
+  `gesetze-rechtsprechung.sh.juris.de` ist seit Ende 2022 eine reine Single-Page-Anwendung; die
+  Dokumentinhalte liegen hinter einer anmeldepflichtigen „mylex“-API (`connectUrl` aus dem
+  `localStorage`), sämtliche Dokument-URLs liefern nur die leere Anwendungshülle. Die Wayback
+  Machine hat die serverseitig gerenderte Gesamtausgabe der Gemeindeordnung zuletzt am
+  14.11.2022 erfasst — dort fehlt der hier geänderte § 34a noch (er wurde erst durch das Gesetz
+  vom 05.02.2025 eingefügt). Auch die archivierten Einzelnorm-Seiten (113 Stück) sind bereits
+  SPA-Hüllen ohne Inhalt. Ein voller Akzeptanztest bleibt daher offen, bis die konsolidierte
+  Fassung aus einer anderen Quelle vorliegt.
 * *Rheinland-Pfalz*: Die Verkündungsplattform `verkuendung.rlp.de` verkündet erst seit dem
   01.07.2026 elektronisch; ältere GVBl-Hefte liegen nicht als freie Direkt-PDF vor.
 * Bremen, Hamburg, Mecklenburg-Vorpommern, Saarland, Brandenburg, Sachsen-Anhalt: noch nicht
diff --git a/src/test/java/eu/mulk/aendggner/EndToEndTest.java b/src/test/java/eu/mulk/aendggner/EndToEndTest.java
index 90ad4ed..d36341d 100644
--- a/src/test/java/eu/mulk/aendggner/EndToEndTest.java
+++ b/src/test/java/eu/mulk/aendggner/EndToEndTest.java
@@ -3,16 +3,19 @@
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.jupiter.api.Assumptions.assumeTrue;
 
+import eu.mulk.aendggner.aenderung.Aenderungsbefehl;
 import eu.mulk.aendggner.aenderung.Aenderungsbefehl.UnbekannterBefehl;
 import eu.mulk.aendggner.aenderung.parse.AenderungsgesetzParser;
 import eu.mulk.aendggner.aenderung.parse.PatchTextExtraktor;
 import eu.mulk.aendggner.aenderung.parse.TextBereiniger;
 import eu.mulk.aendggner.anwendung.BefehlAnwender;
+import eu.mulk.aendggner.gesetz.Gesetz;
 import eu.mulk.aendggner.gesetz.gii.GiiXmlLoader;
 import eu.mulk.aendggner.synopse.HtmlRenderer;
 import eu.mulk.aendggner.synopse.SynopseBuilder;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.util.List;
 import org.junit.jupiter.api.Test;
 
 /**
@@ -428,4 +431,55 @@
     var synopse = SynopseBuilder.baue(gesetz, anwendung, parseErgebnis.warnungen(), false);
     assertThat(HtmlRenderer.rendere(synopse, "E2E-Test NEFG")).contains("§ 1");
   }
+
+  /**
+   * Schleswig-Holstein: Extraktion und Befehlserkennung auf dem Kommunalrecht-Änderungsgesetz
+   * (GVOBl. Schl.-H. 2026/27). Kein voller Akzeptanztest — die konsolidierte Gemeindeordnung ist
+   * aus freien Quellen nicht in der maßgeblichen Fassung zu beschaffen (das juris-Landesportal
+   * liefert nur eine anmeldepflichtige API; die letzten frei archivierten Volltexte stammen aus
+   * 2022 und kennen den hier geänderten § 34a noch nicht). Geprüft wird deshalb alles bis
+   * einschließlich der Befehlserkennung, gegen ein Stammgesetz ohne Normen.
+   */
+  @Test
+  void kommunalrechtAendGSchleswigHolstein() throws Exception {
+    var pdf = SAMPLEDATA.resolve("SchleswigHolstein/GVOBl-2026-27_Kommunalrecht-AendG.pdf");
+    assumeTrue(Files.exists(pdf), "GVOBl-Schl.-H.-Beispiel-PDF fehlt");
+
+    var text = TextBereiniger.bereinige(new PatchTextExtraktor().extrahiere(pdf));
+
+    // Laufender Seitenkopf entfernt — er stand zwischen Artikel 1 und Artikel 2 und klebte sonst
+    // an der Artikel-2-Überschrift.
+    assertThat(text).doesNotContain("Gesetz- und Verordnungsblatt für Schleswig-Holstein");
+    assertThat(text).doesNotContain("2026/27 vom 30. März");
+    // Hochgestellte Fußnotenmarker der Artikel-Überschriften („Artikel 1 ¹)“) sind abgestreift.
+    assertThat(text).contains("\nArtikel 1\n").contains("\nArtikel 2\n").contains("\nArtikel 3\n");
+    assertThat(text).doesNotContain("¹)").doesNotContain("²)").doesNotContain("³)");
+    // Silbentrennung im Flattersatz zusammengezogen (dort findet die geometrische Randerkennung
+    // keinen Ausrichtungs-Cluster und meldet auch volle Zeilen als hartes Zeilenende).
+    assertThat(text).contains("Ton-Bild-Übertragung zu ermöglichen").doesNotContain("Übertra-");
+
+    // Artikel 1 ändert die Gemeindeordnung, Artikel 2 die Kreisordnung, Artikel 3 ein anderes
+    // Änderungsgesetz — nur der jeweils zutreffende Artikel wird gewählt.
+    var gemeindeordnung =
+        new Gesetz("GO", "Gemeindeordnung für Schleswig-Holstein", "Gemeindeordnung", List.of());
+    var kreisordnung =
+        new Gesetz("KrO", "Kreisordnung für Schleswig-Holstein", "Kreisordnung", List.of());
+
+    var go = new AenderungsgesetzParser().parse(text, gemeindeordnung, null);
+    assertThat(go.artikel()).containsExactly("1");
+    assertThat(go.befehle()).hasSize(1);
+    assertThat(go.befehle()).noneMatch(b -> b instanceof UnbekannterBefehl);
+    var neufassung = go.befehle().get(0);
+    assertThat(neufassung.stelle().anzeigeText()).isEqualTo("§ 34a Absatz 1");
+    assertThat(neufassung).isInstanceOf(Aenderungsbefehl.Neufassung.class);
+    // Das Zitat der neuen Fassung ist vollständig übernommen („erhält folgende Fassung“).
+    assertThat(((Aenderungsbefehl.Neufassung) neufassung).neuerText())
+        .startsWith("(1) Durch Hauptsatzung kann bestimmt werden")
+        .endsWith("persönlich im Sitzungsraum anwesend sein.");
+
+    var kro = new AenderungsgesetzParser().parse(text, kreisordnung, null);
+    assertThat(kro.artikel()).containsExactly("2");
+    assertThat(kro.befehle()).hasSize(1);
+    assertThat(kro.befehle().get(0).stelle().anzeigeText()).isEqualTo("§ 29a Absatz 1");
+  }
 }
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 755db46..f49e41b 100644
--- a/src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java
+++ b/src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java
@@ -202,6 +202,40 @@
   }
 
   @Test
+  void ziehtSilbentrennungAuchBeiHartemZeilenendeZusammen() {
+    // Flattersatz (GVOBl. Schl.-H.): Die Randerkennung findet keinen Ausrichtungs-Cluster und
+    // meldet auch volle Zeilen als hartes Zeilenende. Klebt der Trennstrich ohne Trailing-
+    // Whitespace am Zeilenende, ist er dennoch eine Silbentrennung.
+    assertThat(TextBereiniger.bereinige("Teilnahme mittels Ton-Bild-Übertra-\ngung. "))
+        .isEqualTo("Teilnahme mittels Ton-Bild-Übertragung.");
+    // Mit Trailing-Whitespace bleibt das harte Zeilenende ein bewusster Umbruch.
+    assertThat(TextBereiniger.bereinige("Wirk-, Ausgangs- \nund Hilfsstoffe "))
+        .isEqualTo("Wirk-, Ausgangs-\nund Hilfsstoffe");
+  }
+
+  @Test
+  void streiftFussnotenmarkerVonArtikelUeberschriften() {
+    // Schleswig-Holstein verweist an der Artikel-Überschrift auf die Gl.-Nr.-Fußnote; steht die
+    // Einleitung in derselben Zeile, wird sie dabei wieder abgetrennt.
+    assertThat(TextBereiniger.bereinige("Artikel 1 ¹)\nDie Gemeindeordnung"))
+        .isEqualTo("Artikel 1\nDie Gemeindeordnung");
+    assertThat(TextBereiniger.bereinige("Artikel 2 ²) Die Kreisordnung"))
+        .isEqualTo("Artikel 2\nDie Kreisordnung");
+  }
+
+  @Test
+  void entferntSeitenkopfDesGvoblSchleswigHolstein() {
+    var roh =
+        """
+        Erster Satz.
+        Gesetz- und Verordnungsblatt für Schleswig-Holstein
+        2026/27 vom 30. März
+        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.“"))