Classify line ends by alignment clusters and canonicalize definition lists
The percentile-based right-margin estimate from the previous commit
misclassified two real-world layouts: at the column switch of the
two-column old-BGBl format a window contains both column margins, so the
90th percentile returned the right column's margin and marked every
left-column soft wrap as deliberate (vetoing hyphen mending, IfSG 2020:
"Impf- surveillance"); and series of equally wide centered "Artikel N"
headings in draft bills formed the top of their window's distribution,
so they counted as full-width and were reflowed into the following line,
breaking teileInArtikel's line anchor (ProdHaftG RegE). Classify against
alignment clusters instead: a line end is soft when at least five window
lines end within 4 pt of it (the justified block's own margin), hard when
such a cluster runs at least 10 pt above it, unclassified otherwise.
Structure anchors ("Artikel 2", "§ 19", bare Gliederung labels) are
always exempt from reflow since equal-width heading series still form
sham clusters. Also flush the pending line end-X at page ends — without
it, the first line of the next page inherited the previous page's footer
geometry and was misclassified (UWG: "Artikel 2" swallowed the closing
provisions).
Markerless joins additionally never cross into a following enumeration
marker line ("...vorgesehen und" + "d) die Überwachung" no longer glues
to "undd)"), a text-level veto that also works without geometry.
On the base-law side, ContentFlattener now separates sibling <LA>
elements within a <DD> — the short-label/definition pairs of the UWG
Anhang and § 2 IfSG were previously glued without any separator
("Irreführung über Unternehmereigenschaftdie unwahre Angabe...") in both
synopsis columns. The continuation line is indented two spaces deeper
than its enumeration line so StellenAufloeser.zeilenBlock keeps it inside
the unit's block ("Anhang Nummer 31 Buchstabe b"). BefehlAnwender writes
the same canonical shape when inserting or recasting quoted units
(rueckeZitatEin), so XML-derived and PDF-derived items agree:
" 2a. Stichwort" + " Definitionstext".
Verified by mvnw verify (179 tests, 7 new) and a before/after sweep of
all 21 sample law/amendment combinations: applied/manual counts are
unchanged throughout (UWG 19/0, GEG-BGBl 66/53, GModG-RegE 77/20,
IfSG-0645 42/24, ...), no marker characters leak into the HTML output.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Change-Id: Ib7faae1bc5c59bda83f648a579af16a4c77e4285
diff --git a/FASSUNGEN.txt b/FASSUNGEN.txt
index d3195bb..9ea3b7e 100644
--- a/FASSUNGEN.txt
+++ b/FASSUNGEN.txt
@@ -32,7 +32,7 @@
Artikel 1
-Geometrische Spaltenbreiten-Klassifikation für markerlose Zeilenumbrüche
+Geometrische Klassifikation der Zeilenenden nach Ausrichtungs-Clustern
Der Textbereiniger (TextBereiniger.verbindeUmbrueche) zog eine Zeile, die ohne
Leerraum am Ende mit einem Buchstaben schließt und deren Folgezeile klein
@@ -41,37 +41,61 @@
gebrachte Silbentrennung. Diese Annahme trifft auf einen bewussten Wortgrenzen-
Umbruch (etwa das Stichwort einer hängend eingerückten Definition, wie er im
Anhang zum UWG vorkommt) nicht zu. Zur Behebung bestimmt der PDF-Vorverarbeiter
-(FontgroessenFilter) das geometrische End-X der Zeilenläufe und klassifiziert
-die Zeilenenden anhand des lokalen rechten Randes (90. Perzentil in einem
-Fenster von 20 Zeilen davor/danach) in weiche (am Rand endende), harte (deutlich
-davor endende) oder unklassifizierte Umbrüche. Der Textbereiniger zieht nur noch
-geometrisch weiche oder (in Ermangelung von Geometriedaten als Rückfalloption)
-zeichenzahlnäherungsweise am Rand liegende markerlose Umbrüche zusammen. Harte
-Umbrüche bleiben als gewollte Wortgrenzen-Umbrüche erhalten.
+(FontgroessenFilter) das geometrische End-X jeder Zeile (unter Flush auch am
+Seitenende, damit die letzte Zeile einer Seite nicht die erste der Folgeseite
+verfälscht) und klassifiziert die Zeilenenden anhand von Ausrichtungs-Clustern
+in einem Fenster von 20 Zeilen davor/danach: Ein Zeilenende ist weich, wenn
+mindestens fünf Fensterzeilen (auf vier Punkt genau) gleich ausgerichtet enden
+(Satzspiegelrand des Blocksatzes), und hart, wenn ein solcher Rand mindestens
+zehn Punkt oberhalb des Zeilenendes verläuft — dort wäre noch Platz gewesen,
+der Umbruch ist mithin gewollt. Cluster statt eines Fenster-Perzentils, weil am
+Spaltenwechsel des zweispaltigen alten Bundesgesetzblatts beide Spaltenränder
+in ein Fenster fallen; maßgeblich ist der Rand, an dem die Zeile selbst
+ausgerichtet ist. Der Textbereiniger zieht markerlose Umbrüche nur noch bei
+weichen Zeilenenden zusammen (in Ermangelung von Geometriedaten näherungsweise
+nach Zeichenzahl wie bisher), nie jedoch vor einer Folgezeile, die mit einem
+Aufzählungsmarker („d)“, „3.“) beginnt.
Artikel 2
+Zusammenzug weicher und Erhaltung harter Umbrüche
+
+Nach den Einzelzusammenzügen fügt der Textbereiniger geometrisch weiche
+Zeilenenden mit einem Leerzeichen zu Fließtext zusammen (Reflow); harte und
+unklassifizierte Umbrüche bleiben erhalten. Nach der Bereinigung ist damit
+jeder verbleibende Zeilenumbruch nach bester Einschätzung beabsichtigt, weshalb
+die Zitat-Normalisierung des Befehlsanwenders (normalisiereZitatText) Umbrüche
+nicht mehr zu Leerzeichen faltet. Ausgenommen vom Reflow sind Strukturzeilen
+(„Artikel 2“, „§ 19“, allein stehende Gliederungs-Bezeichnungen): Gleich
+breite, zentrierte Überschriften in Serie bilden ein Schein-Cluster, der Parser
+benötigt sie jedoch allein auf der Zeile.
+
+
+Artikel 3
Strukturierung und Normalisierung hängend eingerückter Definitionslisten
Damit die nunmehr erhaltenen hängenden Zeilenenden in Definitionslisten (wie
-beim UWG-Anhang) korrekt strukturiert werden:
-1. Der Befehlsanwender (BefehlAnwender.normalisiereZitatText) unterwirft nunmehr
- auch die stellenlose Neufassung (z.B. eines Satzes) der Zitat-Normalisierung
- und rückt fortlaufende Zeilen innerhalb eines Aufzählungspunktes tiefer
- (vier Leerzeichen) ein als den Aufzählungspunkt selbst (zwei Leerzeichen).
-2. Der XML-Flattener (ContentFlattener) trennt mehrere <LA>-Geschwister innerhalb
- eines <DD>-Elements durch Zeilenumbrüche und rückt sie tiefer ein.
-Beide Wege erzeugen dieselbe kanonische Zeilenform, welche für die korrekte
-Erkennung von Kindzeilen durch die Stellenauflösung erforderlich ist.
+beim UWG-Anhang oder in § 2 IfSG) korrekt strukturiert werden:
+1. Der XML-Flattener (ContentFlattener) trennt mehrere <LA>-Geschwister innerhalb
+ eines <DD>-Elements durch Zeilenumbrüche und rückt die Folgezeilen tiefer ein
+ als die Aufzählungszeile (Kindzeilen der Einheit für die Stellenauflösung).
+2. Der Befehlsanwender rückt beim Einsetzen von Zitattext (rueckeZitatEin)
+ Fortsetzungszeilen zwei Zeichen tiefer ein als die Aufzählungszeile.
+Beide Wege erzeugen dieselbe kanonische Zeilenform („ 2a. Stichwort“ nebst
+„ Definitionstext“), welche für die korrekte Erkennung von Kindzeilen durch
+die Stellenauflösung (StellenAufloeser.zeilenBlock) erforderlich ist.
Schlussbestimmung
Die vorstehenden Änderungen sind durch die Prüfung sämtlicher Testfälle (einhun-
-dertvierundsiebzig an der Zahl) sowie durch die vollständige Erstellung (mvnw
-verify) bestätigt worden. Anlass war ein anhand der UWG-Novelle 2026 gemeldeter
-Wiedergabefehler; die Behebung wurde anhand der Beispieldaten (UWG-Anhang,
-Nummern 2a sowie 4a bis 4c) verifiziert.
+dertneunundsiebzig an der Zahl), durch die vollständige Erstellung (mvnw verify)
+sowie durch einen Vorher-nachher-Vergleich sämtlicher einundzwanzig Beispiel-
+kombinationen (UWG, AGG, ProdHaftG, GEG, IfSG; Anwendungs- und Prüfzahlen
+durchgehend unverändert) bestätigt worden. Anlass war ein anhand der UWG-Novelle
+2026 gemeldeter Wiedergabefehler (verklebte Kurzüberschriften im Anhang, auch in
+der Grundvorschrift); die Behebung wurde anhand der Beispieldaten (UWG-Anhang,
+Nummern 2a, 4c sowie 22; § 2 IfSG) verifiziert.
════════════════════════════════════════════════════════════════════════════════