fix(geo+recall): Länder-Centroid statt Hauptstadt + Eigennamen in GNews-Query

Zwei Fixes aus der jp_demo-Verifikation:

1. Geoparsing — Länder mit Centroid statt Hauptstadt
   Bisher bekam ein Land die Koordinaten seiner Hauptstadt. Damit landeten
   alle "Japan"-Marker exakt auf Tokyo (35.69, 139.69) und die Karte
   suggerierte faelschlich ein Ereignis in der Hauptstadt. Neue Tabelle
   _COUNTRY_CENTROIDS (37 Laender) verortet ein Land in seiner geografischen
   Mitte (Japan: 36.20, 138.25). Laender ohne Centroid-Eintrag fallen auf die
   Hauptstadt zurueck.

2. Recall — Eigennamen in den Google-News-Suchfeed erzwingen
   Beim ersten Refresh fehlt die Headlines-Historie, daher kamen die GNews-
   Such-Keywords aus der Feed-Selektion. Haiku legt Eigennamen (z.B. "Qilin")
   in die en-Liste, die ja-Liste hatte nur Allgemeinbegriffe — die ja-Query
   suchte ohne "Qilin". build_news_search_feeds stellt nicht-englischen
   Sprach-Queries jetzt die 2 wichtigsten en-Keywords voran (Eigennamen
   kommen auch in fremdsprachigen Artikeln lateinisch vor). Damit ist schon
   der erste Refresh spezifisch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
2026-05-22 02:13:30 +02:00
Ursprung 4e9d9f92f1
Commit 309c97f40a
2 geänderte Dateien mit 68 neuen und 11 gelöschten Zeilen

Datei anzeigen

@@ -31,6 +31,28 @@ def _get_geonamescache():
return _gc return _gc
# Geografische Zentren (Centroids) der Laender, keyed nach ISO-2-Code.
# Wird genutzt, wenn ein Artikel ein LAND nennt (kein konkreter Ort). Vorher
# wurde dem Land die Hauptstadt zugewiesen — das stapelte z.B. alle "Japan"-
# Marker exakt auf Tokyo und suggerierte faelschlich ein Ereignis in der
# Hauptstadt. Das Centroid liegt in der Landesmitte und ist neutral.
# Laender, die hier fehlen, fallen auf die Hauptstadt zurueck (alte Logik).
_COUNTRY_CENTROIDS = {
"AF": (33.94, 67.71), "AT": (47.52, 14.55), "AZ": (40.14, 47.58),
"CH": (46.82, 8.23), "CN": (35.86, 104.20), "CY": (35.13, 33.43),
"DE": (51.17, 10.45), "EG": (26.82, 30.80), "ES": (40.46, -3.75),
"FR": (46.23, 2.21), "GB": (54.70, -3.28), "GR": (39.07, 21.82),
"IL": (31.05, 34.85), "IN": (20.59, 78.96), "IQ": (33.22, 43.68),
"IR": (32.43, 53.69), "IT": (41.87, 12.57), "JO": (30.59, 36.24),
"JP": (36.20, 138.25), "KP": (40.34, 127.51), "KR": (35.91, 127.77),
"KW": (29.31, 47.48), "LB": (33.85, 35.86), "NL": (52.13, 5.29),
"OM": (21.47, 55.98), "PK": (30.38, 69.35), "PS": (31.95, 35.23),
"QA": (25.32, 51.18), "RU": (61.52, 105.32), "SA": (23.89, 45.08),
"SY": (34.80, 38.997), "TR": (38.96, 35.24), "UA": (48.38, 31.17),
"US": (39.83, -98.58), "YE": (15.55, 48.52), "TW": (23.80, 121.00),
}
# Bekannte Laendernamen (deutsch/englisch/alternativ -> ISO-2 Code + Hauptstadt-Koordinaten) # Bekannte Laendernamen (deutsch/englisch/alternativ -> ISO-2 Code + Hauptstadt-Koordinaten)
_COUNTRY_ALIASES = { _COUNTRY_ALIASES = {
"libanon": {"code": "LB", "name": "Lebanon", "lat": 33.8938, "lon": 35.5018}, "libanon": {"code": "LB", "name": "Lebanon", "lat": 33.8938, "lon": 35.5018},
@@ -106,9 +128,12 @@ def _geocode_offline(name: str, country_code: str = "") -> Optional[dict]:
# 1. Bekannte Laender-Aliase (schnellster + sicherster Pfad) # 1. Bekannte Laender-Aliase (schnellster + sicherster Pfad)
alias = _COUNTRY_ALIASES.get(name_lower) alias = _COUNTRY_ALIASES.get(name_lower)
if alias: if alias:
# Land -> geografisches Zentrum (Centroid) statt Hauptstadt, wo bekannt.
centroid = _COUNTRY_CENTROIDS.get(alias["code"])
lat, lon = centroid if centroid else (alias["lat"], alias["lon"])
return { return {
"lat": alias["lat"], "lat": lat,
"lon": alias["lon"], "lon": lon,
"country_code": alias["code"], "country_code": alias["code"],
"normalized_name": alias["name"], "normalized_name": alias["name"],
"confidence": 0.95, "confidence": 0.95,
@@ -118,9 +143,20 @@ def _geocode_offline(name: str, country_code: str = "") -> Optional[dict]:
countries = gc.get_countries() countries = gc.get_countries()
for code, country in countries.items(): for code, country in countries.items():
if country.get("name", "").lower() == name_lower: if country.get("name", "").lower() == name_lower:
# Land -> Centroid (Landesmitte), wo bekannt. Das verhindert, dass
# alle "Japan"-Marker exakt auf Tokyo gestapelt werden.
centroid = _COUNTRY_CENTROIDS.get(code)
if centroid:
return {
"lat": centroid[0],
"lon": centroid[1],
"country_code": code,
"normalized_name": country["name"],
"confidence": 0.9,
}
# Kein Centroid hinterlegt -> Fallback auf die Hauptstadt.
capital = country.get("capital", "") capital = country.get("capital", "")
if capital: if capital:
# Hauptstadt geocoden, aber als Land benennen
cap_alias = _COUNTRY_ALIASES.get(capital.lower()) cap_alias = _COUNTRY_ALIASES.get(capital.lower())
if cap_alias: if cap_alias:
return { return {

Datei anzeigen

@@ -58,15 +58,36 @@ def build_news_search_feeds(
locale = _GNEWS_LOCALE.get(lang_key) locale = _GNEWS_LOCALE.get(lang_key)
if not locale: if not locale:
continue continue
kws = keywords_by_lang.get(lang_key) or [] lang_kws = [str(k).strip() for k in (keywords_by_lang.get(lang_key) or []) if str(k).strip()]
# Fallback: wenn fuer die Sprache keine Keywords da sind, "en" nehmen en_kws = [str(k).strip() for k in (keywords_by_lang.get("en") or []) if str(k).strip()]
# (lateinische Eigennamen matchen auch in fremdsprachigen News-Indizes).
if not kws and lang_key != "en": if lang_key == "en":
kws = keywords_by_lang.get("en") or [] query_terms = en_kws[:max_keywords]
kws = [str(k).strip() for k in kws if str(k).strip()] else:
if not kws: # Fuer nicht-englische Sprachen: die ersten 2 englischen Keywords
# voranstellen. Haiku ordnet Eigennamen/Akronyme (z.B. "Qilin",
# "Asahi") nach vorne — und die kommen auch in fremdsprachigen
# Artikeln lateinisch vor. Ohne das fehlt beim ersten Refresh (noch
# keine Headlines-Historie) der entscheidende Eigenname in der Query.
# Danach 3 sprach-spezifische Keywords.
query_terms = en_kws[:2] + lang_kws[:3]
# Wenn fuer die Sprache gar keine Keywords da sind: ganz auf en.
if not lang_kws:
query_terms = en_kws[:max_keywords]
# Dedup, Reihenfolge erhalten
seen_terms: set[str] = set()
deduped: list[str] = []
for t in query_terms:
tl = t.lower()
if tl in seen_terms:
continue
seen_terms.add(tl)
deduped.append(t)
if not deduped:
continue continue
query = " ".join(kws[:max_keywords]) query = " ".join(deduped)
if not query or query in seen_queries: if not query or query in seen_queries:
continue continue
seen_queries.add(query) seen_queries.add(query)