From 6311227992ba509b5e5e3256504980bf4632742d Mon Sep 17 00:00:00 2001 From: Matthias Andreas Benkard Date: Fri, 17 Jul 2026 05:10:53 +0200 Subject: Fix markerless line-break join gluing whole words without a space MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TextBereiniger.verbindeUmbrueche assumed any letter-ending, no-trailing-space line followed by a lowercase continuation was a hyphen-less mid-word split (PDF extraction artifact) and joined it with zero separator. That assumption breaks for deliberate word-boundary breaks, e.g. the short-label/hanging- indent definition format in the UWG Anhang ("...Nachhaltigkeitssiegels" + "das Anbringen..."), producing glued words in the rendered synopsis. Gate the markerless join on the candidate line reaching a locally-typical "full column width" (90th percentile in a ±20-line window), since automatic wraps always land near the column edge while deliberate breaks don't. The window is local rather than document-wide because some source PDFs mix column widths within one document (narrower Regelungstext vs. wider Begründung), which a global statistic would otherwise penalize. Also route the single-unit Neufassung fallback through the existing normalisiereZitatText normalization, matching its sibling code paths, so that internal line breaks now more often preserved by the fix above don't leak into the HTML output as spurious line breaks instead. Co-Authored-By: Claude Sonnet 5 Change-Id: I0a79f22f9286cd4a90eea32bad9b54cb4a082cf4 --- .../aendggner/aenderung/parse/TextBereiniger.java | 53 +++++++++++++++++++++- .../mulk/aendggner/anwendung/BefehlAnwender.java | 3 +- 2 files changed, 53 insertions(+), 3 deletions(-) (limited to 'src/main/java') 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 6fb511d..d9ecf8a 100644 --- a/src/main/java/eu/mulk/aendggner/aenderung/parse/TextBereiniger.java +++ b/src/main/java/eu/mulk/aendggner/aenderung/parse/TextBereiniger.java @@ -73,6 +73,17 @@ public final class TextBereiniger { private static final Pattern KONJUNKTION = Pattern.compile("^(und|oder|sowie|bzw\\.|beziehungsweise)\\b.*"); + /** Perzentil der Zeilenlängen, das als „volle Spaltenbreite“ gilt (siehe {@link #verbindeUmbrueche}). */ + private static final double VOLLZEILE_PERZENTIL = 0.9; + + /** Mindestanteil der vollen Spaltenbreite, ab dem ein markerloser Umbruch als Silbentrennung + * statt als bewusster Wortgrenzen-Umbruch gilt. */ + private static final double VOLLZEILE_MINDESTANTEIL = 0.7; + + /** Anzahl Zeilen vor/nach einer Kandidatenzeile, die für die lokale Spaltenbreiten-Schätzung + * herangezogen werden (siehe {@link #typischeZeilenlaenge}). */ + private static final int VOLLZEILE_FENSTER = 20; + // BMJV-Entwurfsvorlagen zeichnen das hängende öffnende Anführungszeichen im Content-Stream // NACH dem ersten Element der zitierten Passage: „(1) „ Ungeachtet…“ statt „„(1) Ungeachtet…“, // „§ 19„“ statt „„§ 19“. @@ -183,7 +194,11 @@ public final class TextBereiniger { *
  • Markerlos (Bundestags-Drucksachen: „Schwel“ + „lenwertes“): Reguläre Umbrüche * enden dort mit Leerzeichen vor dem Zeilenumbruch; endet eine Zeile direkt mit einem * Buchstaben und beginnt die Folgezeile klein, ist es eine Trennung → ohne Leerzeichen - * zusammenziehen. + * zusammenziehen. Das trifft aber nur zu, wenn die Zeile (fast) die volle Spaltenbreite + * ausnutzt — sonst wäre der Umbruch dort nicht nötig gewesen. Kurze, bewusst + * abgebrochene Zeilen (z.B. ein Stichwort vor einer hängend eingerückten Definition: + * „…Nachhaltigkeitssiegels“ + „das Anbringen …“) werden deshalb ausgenommen — sie sind + * ein Wortgrenzen-Umbruch, keine Silbentrennung, auch wenn das Trailing-Space-Signal fehlt. * */ private static ArrayList verbindeUmbrueche(List zeilen) { @@ -198,7 +213,10 @@ public final class TextBereiniger { while (true) { var gestutzt = zeile.stripTrailing(); var mitTrennstrich = endetMitSilbentrennung(gestutzt); - var markerlos = markerlosAktiv && endetMarkerlos(zeile); + var markerlos = + markerlosAktiv + && endetMarkerlos(zeile) + && gestutzt.length() >= typischeZeilenlaenge(zeilen, i) * VOLLZEILE_MINDESTANTEIL; if (!mitTrennstrich && !markerlos) { break; } @@ -266,6 +284,37 @@ public final class TextBereiniger { return mitTrailingSpace > 0 && mitTrailingSpace * 4 >= nichtLeer; } + /** + * Typische „volle“ Zeilenlänge im Umfeld von {@code zentrum} ({@link #VOLLZEILE_PERZENTIL}- + * Perzentil der gestutzten Längen nichtleerer Zeilen in einem Fenster von {@link + * #VOLLZEILE_FENSTER} Zeilen davor/danach) — ein grober Näherungswert für die lokale + * Spaltenbreite, ohne auf PDF-Positionsdaten zugreifen zu müssen. Lokal statt dokumentweit, weil + * ein einziges Dokument Abschnitte mit unterschiedlicher Spaltenbreite mischen kann (z.B. + * schmalerer Regelungstext vs. breitere Begründung in Regierungsentwürfen) — eine dokumentweite + * Kennzahl würde dort die kürzere Spalte systematisch benachteiligen. Vereinzelte überlange + * Zeilen (z.B. selbst fälschlich verklebte Umbrüche) dürfen den Wert nicht verzerren, daher ein + * hohes Perzentil statt des reinen Maximums. + */ + private static int typischeZeilenlaenge(List zeilen, int zentrum) { + var laengen = new ArrayList(); + int von = Math.max(0, zentrum - VOLLZEILE_FENSTER); + int bis = Math.min(zeilen.size(), zentrum + VOLLZEILE_FENSTER + 1); + for (int i = von; i < bis; i++) { + var zeile = zeilen.get(i); + if (zeile.isBlank()) { + continue; + } + laengen.add(zeile.stripTrailing().length()); + } + if (laengen.isEmpty()) { + return 0; + } + laengen.sort(null); + int index = (int) (laengen.size() * VOLLZEILE_PERZENTIL); + index = Math.min(index, laengen.size() - 1); + return laengen.get(index); + } + private static String strippeZeilenenden(List zeilen) { var sb = new StringBuilder(); for (var zeile : zeilen) { diff --git a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java index 4140357..68fe2b8 100644 --- a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java +++ b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java @@ -460,7 +460,8 @@ public final class BefehlAnwender { } // Neufassung eines Satzes / einer Nummer / eines Buchstabens: Bereich ersetzen. - return bearbeiteText(normen, befehl, text -> TextErgebnis.ok(befehl.neuerText().strip())); + return bearbeiteText( + normen, befehl, text -> TextErgebnis.ok(normalisiereZitatText(befehl.neuerText()))); } /** Ein Ziel (Absatz, Satz, Nummer, Buchstabe) wird durch einen Block ersetzt (ggf. 1 → N). */ -- cgit v1.2.1