From 58dc5374139d469702596778efb953f8b1ce2cc7 Mon Sep 17 00:00:00 2001
From: untergeekDE <jan@eggers-elektronik.de>
Date: Sun, 26 Mar 2023 17:45:33 +0200
Subject: [PATCH] Leicht angepasst nach Kassel und Darmstadt

---
 R/Analyse/Auswertung.R                        | 153 ++++++++++++++++++
 R/aktualisiere_karten.R                       |  24 +++
 R/lies_aktuellen_stand.R                      |  23 ++-
 R/lies_konfiguration.R                        |  14 +-
 R/main.R                                      |   5 +-
 R/main_oneshot.R                              |  34 +++-
 R/messaging.R                                 |   5 +-
 README.md                                     |  37 ++++-
 howto_shapefiles.md                           |  12 +-
 index/obwahl_ffm_2023/ffm_config.csv          |  37 +++++
 index/obwahl_ffm_2023/ffm_config_test.csv     |  37 +++++
 index/obwahl_ks_2023/config.csv               |  23 +++
 index/obwahl_ks_2023/config_test.csv          |  23 +++
 index/obwahl_ks_2023/kandidaten.xlsx          | Bin 6265 -> 6261 bytes
 .../obwahl_ks_2023/parteien-kommunalwahl.xlsx | Bin 0 -> 6155 bytes
 15 files changed, 402 insertions(+), 25 deletions(-)
 create mode 100644 R/Analyse/Auswertung.R
 create mode 100644 index/obwahl_ffm_2023/ffm_config.csv
 create mode 100644 index/obwahl_ffm_2023/ffm_config_test.csv
 create mode 100644 index/obwahl_ks_2023/config.csv
 create mode 100644 index/obwahl_ks_2023/config_test.csv
 create mode 100644 index/obwahl_ks_2023/parteien-kommunalwahl.xlsx

diff --git a/R/Analyse/Auswertung.R b/R/Analyse/Auswertung.R
new file mode 100644
index 0000000..a93516b
--- /dev/null
+++ b/R/Analyse/Auswertung.R
@@ -0,0 +1,153 @@
+# Auswertung nach Stadtteilen vs. Kommunalwahlergebnis 2021
+
+
+# Parteienliste 
+parteien_df <- read.xlsx("index/obwahl_da_2023/parteien-kommunalwahl.xlsx")
+
+# Kommunaldaten: Stadtverordnetenwahl
+kommunal_url <- "https://votemanager-da.ekom21cdn.de/2021-03-14/06411000/praesentation/Open-Data-06411000-Stadtverordnetenwahl-Wahlbezirk.csv?ts=1679256774922"
+k_stimm_df <- lies_stimmbezirke(kommunal_url) %>% 
+  # Die Spalten D1-D14 enthalten die Gesamtergebnisse der Parteien. 
+  # Unzählige weitere Spalten enthalten die Ergebnisse für jeden Kandidaten
+  # auf den sehr, sehr langen Wahllisten. 
+  select(zeitstempel,
+         nr,
+         name,
+         meldungen_anz,
+         meldungen_max,
+         wahlberechtigt,
+         waehler_regulaer,
+         waehler_wahlschein,
+         waehler_nv,
+         stimmen,
+         stimmen_wahlschein,
+         ungueltig,
+         gueltig,
+         matches("D[0-9]+$")) %>% 
+  mutate(nr = as.integer(str_extract(nr,"[0-9]+")))
+
+
+k_stadtteile_df <- k_stimm_df %>% 
+  left_join(stimmbezirke_df %>% select(nr,ortsteilnr,stadtteil),
+            by="nr") %>% 
+  group_by(ortsteilnr)   %>% 
+  # Fasse alle Spalten von meldungen_anz bis Ende der Tabelle zusammen - 
+  # mit der sum()-Funktion (NA wird wie null behandelt)
+  summarize(zeitstempel = last(zeitstempel),
+            nr = first(ortsteilnr), 
+            meldungen_anz = sum(meldungen_anz,na.rm =T),
+            meldungen_max = sum(meldungen_max,na.rm = T),
+            wahlberechtigt = sum(wahlberechtigt, na.rm = T),
+            waehler_regulaer = sum(waehler_regulaer, na.rm = T),
+            waehler_wahlschein = sum(waehler_wahlschein, na.rm = T),
+            waehler_nv = sum(waehler_nv, na.rm = T),
+            stimmen = sum(stimmen, na.rm = T),
+            stimmen_wahlschein = sum(stimmen_wahlschein, na.rm = T),
+            ungueltig = sum(ungueltig, na.rm = T),
+            gueltig = sum(gueltig, na.rm = T),
+            across(starts_with("D"), ~ sum(.,na.rm = T))) %>%
+  mutate(across(where(is.numeric), ~ifelse(is.na(.), 0, .))) %>% 
+  # Stadtteilnamen, Geokoordinaten dazuholen
+  left_join(stadtteile_df, by="nr") %>% 
+  # Wichtige Daten für bessere Lesbarkeit nach vorn
+  relocate(zeitstempel,nr,name,lon,lat)
+
+# Sicherheitscheck: Warnen, wenn nicht alle Ortsteile zugeordnet
+if (nrow(stadtteildaten_df) != nrow(stadtteile_df)) teams_warnung("Nicht alle Stadtteile zugeordnet")
+if (nrow(stimmbezirke_df) != length(unique(stimmbezirke_df$nr))) teams_warnung("Nicht alle Stimmbezirke zugeordnet")
+
+  tmp_long_df <- k_stadtteile_df %>%
+    pivot_longer(cols = starts_with("D"), names_to = "partei_nr", values_to = "partei_stimmen") %>% 
+    mutate(partei_nr = as.integer(str_extract(partei_nr,"[0-9]+"))) %>% 
+    # Ortsteil- bzw. Stimmbezirks-Gruppen, um dort nach Stimmen zu sortieren
+    group_by(nr,name) %>% 
+    arrange(desc(partei_stimmen)) %>% 
+    mutate(Platz = row_number()) %>%
+    left_join(parteien_df %>% select(partei_nr = Nummer, 
+                                       partei = Parteikürzel,
+                                       farbe= Farbwert), by="partei_nr") %>% 
+    mutate(prozent = if_else(gueltig != 0,partei_stimmen / gueltig * 100, 0)) 
+  k_ergänzt_df <- tmp_long_df %>% 
+    # Ist noch nach Stadtteil (name, nr) sortiert
+    arrange(partei_nr) %>% 
+    # Alles weg, was verhindert, was individuell auf den Kand ist - außer
+    #  kand und Prozentwert
+    select(-partei_stimmen, -partei_nr, -Platz, -farbe) %>% 
+    # Kandidatennamen in die Spalten zurückverteilen
+    pivot_wider(names_from = partei, values_from = prozent) %>% 
+    ungroup() %>% 
+    # und die zweite Hälfte dazu: 
+    left_join(
+      tst <- tmp_long_df %>% 
+        # Brauchen nur die Kand-Ergebnisse - und den (Stadtteil-)name
+        select(name, Platz, partei,prozent,farbe) %>% 
+        # Nur die ersten (top) Plätze
+        filter(Platz <= (3)) %>% 
+        #The Big Pivot: Breite die ersten (3) aus. 
+        pivot_wider(names_from = Platz,
+                    values_from = c(partei,prozent,farbe),
+                    names_glue = "{.value}{Platz}") %>% 
+        ungroup() %>% 
+        select(-nr),   
+      by="name") %>% 
+    # Jetzt auswählen und umbenennen
+    select(nr, # ortsteilnr
+           k_wahlberechtigt = wahlberechtigt,
+           k_stimmen = stimmen, 
+           k_stimmen_wahlschein = stimmen_wahlschein,
+           k_gueltig = gueltig, 
+           ungueltig:partei3) %>% 
+    rename(k_ungueltig = ungueltig)
+
+ergänzt3_df <- berechne_ergänzt(stadtteildaten_df,3)  
+
+vergleichstabelle_df <- ergänzt3_df %>% 
+  left_join(k_ergänzt_df,by="nr") %>% 
+  select(-zeitstempel,
+         -ortsteilnr,
+         -meldungen_anz,
+         -meldungen_max,
+         -waehler_regulaer,
+         -waehler_wahlschein,
+         -waehler_nv) 
+  # Gesamt-Ergebnisse ergänzen
+
+write.xlsx(vergleichstabelle_df,"daten/obwahl_da_2023/vergleichstabelle2021.xlsx", overwrite=T)
+
+
+# Briefwahlergebnis
+urnenwahl_df <- stimmbezirksdaten_df %>% 
+  # Achtung: Prüfen, ob die Benennung der Briefwahllokale hierzu passt.
+  filter(nr < 9999) %>% 
+  summarize(gueltig = sum(gueltig),
+            across(starts_with("D"),~ sum(.))) %>% 
+  pivot_longer(cols = starts_with("D"), names_to = "kand_nr", 
+               values_to = "kand_stimmen") %>% 
+  mutate(kand_nr = as.integer(str_extract(kand_nr,"[0-9]+"))) %>% 
+  left_join(kandidaten_df %>% select(kand_nr=Nummer,
+                                     Parteikürzel,
+                                     kand_name=Name),by="kand_nr") %>% 
+  mutate(`Kandidat/in` = paste0(kand_name," (",Parteikürzel,")")) %>% 
+  mutate(Prozent = kand_stimmen / gueltig *100) %>% 
+  select(`Kandidat/in`, Urnenwahl = Prozent)
+
+briefwahl_df <- stimmbezirksdaten_df %>% 
+  filter(nr > 9999) %>% 
+  summarize(gueltig = sum(gueltig),
+                  across(starts_with("D"),~ sum(.))) %>% 
+  pivot_longer(cols = starts_with("D"), names_to = "kand_nr", 
+               values_to = "kand_stimmen") %>% 
+  mutate(kand_nr = as.integer(str_extract(kand_nr,"[0-9]+"))) %>% 
+  left_join(kandidaten_df %>% select(kand_nr=Nummer,
+                                     Parteikürzel,
+                                     kand_name=Name),by="kand_nr") %>% 
+  mutate(`Kandidat/in` = paste0(kand_name," (",Parteikürzel,")")) %>% 
+  mutate(Prozent = kand_stimmen / gueltig *100) %>% 
+  select(`Kandidat/in`, Briefwahl = Prozent) %>% 
+  left_join(urnenwahl_df,by = "Kandidat/in")
+
+
+
+write.xlsx(briefwahl_df,"daten/briefwahl_ergebnis.xlsx", overwrite = T)  
+
+            
\ No newline at end of file
diff --git a/R/aktualisiere_karten.R b/R/aktualisiere_karten.R
index 9fa8491..c3e641f 100644
--- a/R/aktualisiere_karten.R
+++ b/R/aktualisiere_karten.R
@@ -277,6 +277,11 @@ aktualisiere_top <- function(kand_tabelle_df,top=5) {
     head(top)
   # Daten pushen
   dw_data_to_chart(daten_df,chart_id = top_id)
+  # Daten aufs Google Bucket (für CORS-Aktualisierung)
+  if (SERVER) {
+    write.csv(daten_df,"daten/top.csv")
+    system('gsutil -h "Cache-Control:no-cache, max_age=0" cp daten/top.csv gs://d.data.gcp.cloud.hr.de/obwahl_top.csv')
+  }
   # Intro_Text nicht anpassen. 
   # Balken reinrendern
   balken_text <- generiere_auszählungsbalken(gezaehlt,stimmbezirke_n,ts)
@@ -297,6 +302,10 @@ aktualisiere_tabelle_alle <- function(kand_tabelle_df) {
   # Daten und Metadaten hochladen, für die Balkengrafik mit allen 
   # Stimmen für alle Kandidaten
   dw_data_to_chart(kand_tabelle_df, chart_id = tabelle_alle_id)
+  if (SERVER) {
+    write.csv(kand_tabelle_df,"daten/kand_tabelle.csv")
+    system('gsutil -h "Cache-Control:no-cache, max_age=0" cp daten/kand_tabelle.csv gs://d.data.gcp.cloud.hr.de/obwahl_kand_tabelle.csv')
+  }
   balken_text <- generiere_auszählung_nurtext(gezaehlt,stimmbezirke_n,ts)
   # Metadaten anpassen: Farbcodes für Parteien
   metadata_chart <- dw_retrieve_chart_metadata(tabelle_alle_id)
@@ -320,7 +329,12 @@ aktualisiere_karten <- function(ergänzt_df) {
   ergänzt_f_df <- ergänzt_df %>% filter(meldungen_anz > 0)
   balken_text = generiere_auszählungsbalken(gezaehlt,stimmbezirke_n,ts)
   dw_edit_chart(chart_id = karte_sieger_id, annotate = balken_text)
+  # Daten pushen
   dw_data_to_chart(ergänzt_f_df,chart_id = karte_sieger_id)
+  if (SERVER) {
+    write.csv(ergänzt_f_df,"daten/ergaenzt.csv")
+    system('gsutil -h "Cache-Control:no-cache, max_age=0" cp daten/ergaenzt.csv gs://d.data.gcp.cloud.hr.de/obwahl_ergaenzt.csv')
+  }
   dw <- dw_publish_chart(karte_sieger_id)
   # Jetzt die Choropleth-Karten für alle Kandidierenden
   for (i in 1:nrow(switcher_df)) {
@@ -333,7 +347,12 @@ aktualisiere_karten <- function(ergänzt_df) {
 
 aktualisiere_hochburgen <- function(hochburgen_df) {
   # Das ist ziemlich geradeheraus. 
+  # Pushe Daten.
   dw_data_to_chart(hochburgen_df, chart_id = hochburgen_id)
+  if (SERVER) {
+    write.csv(hochburgen_df,"daten/hochburgen.csv")
+    system('gsutil -h "Cache-Control:no-cache, max_age=0" cp daten/hochburgen.csv gs://d.data.gcp.cloud.hr.de/obwahl_hochburgen.csv')
+  }
   balken_text <- generiere_auszählung_nurtext(gezaehlt,stimmbezirke_n,ts)
   # Metadaten anpassen: Farbcodes für Parteien
   metadata_chart <- dw_retrieve_chart_metadata(hochburgen_id)
@@ -430,7 +449,12 @@ aktualisiere_ergebnistabelle <- function(stadtteildaten_df) {
     ungroup() %>% 
     arrange(sort) %>% 
     select(-name,-sort)
+  # Daten pushen
   dw_data_to_chart(ergebnistabelle_df %>% select(-nr), chart_id = tabelle_stadtteile_id)
+  if (SERVER) {
+    write.csv(ergebnistabelle_df,"daten/stadtteile.csv")
+    system('gsutil -h "Cache-Control:no-cache, max_age=0" cp daten/stadtteile.csv gs://d.data.gcp.cloud.hr.de/obwahl_stadtteile.csv')
+  }
     # Trendergebnis? Schreibe "Trend" oder "Endergebnis" in den Titel
   gezählt <- e_tmp_df %>% pull(meldungen_anz) %>% sum(.)
   stimmbezirke_n <- e_tmp_df %>% pull(meldungen_max) %>% sum(.)
diff --git a/R/lies_aktuellen_stand.R b/R/lies_aktuellen_stand.R
index 2ffc5fc..685c7c6 100644
--- a/R/lies_aktuellen_stand.R
+++ b/R/lies_aktuellen_stand.R
@@ -85,7 +85,7 @@ check_for_timestamp <- function(my_url) {
     # } else {
     #   t <- tmp[stringr::str_detect(tmp,"last-modified")] %>% 
     #     stringr::str_replace("last-modified: ","") %>% 
-    #     parse_date_time("%a, %d %m %Y %H:%M:%S",tz = "CET") + hours(1)
+    #     parse_date_time("%a, %d %m %Y %H:%M:%S",tz = "CET") 
     # }
   } else { # lokale Datei
     t = file.info(my_url)$mtime %>%  as_datetime
@@ -102,14 +102,15 @@ lies_stimmbezirke <- function(stand_url = stimmbezirke_url) {
   #' Schreibt eine Meldung ins Logfile - zugleich ein Lesezeichen
   cat(as.character(now())," - Neue Daten lesen\n") # Touch logfile
   check = tryCatch(
-    { stand_df <- read_delim(stand_url, 
+    { 
+      stand_df <- read_delim(stand_url, 
                          delim = ";", escape_double = FALSE, 
                          locale = locale(date_names = "de", 
                                          decimal_mark = ",", 
                                          grouping_mark = "."), 
                          trim_ws = TRUE) %>% 
       # Spalten umbenennen, Zeitstempel-Spalte einfügen
-                    mutate(zeitstempel=ts) %>%
+                    mutate(zeitstempel=ts)  %>%
       # Sonderregel: wir haben einen Zeitstempel, die "datum"-Spalte macht
       # Probleme, weil: starts_with("D"). 
                     select(-datum) %>% 
@@ -129,7 +130,9 @@ lies_stimmbezirke <- function(stand_url = stimmbezirke_url) {
                            ungueltig = C,
                            gueltig = D,
                            # neu: alle Zeilen mit Stimmen (D1..Dn)
-                           starts_with("D"))
+                           starts_with("D")) %>% 
+        # Zusatz für Frankfurt, das die Stimmbezirksnummern als character überträgt
+        mutate(nr = as.integer(nr))
       
       },
     warning = function(w) {teams_warning(w,title="OB-Wahl: Datenakquise")},
@@ -166,8 +169,8 @@ aggregiere_stadtteildaten <- function(stimmbezirksdaten_df = stimmbezirksdaten_d
     relocate(zeitstempel,nr,name,lon,lat)
     
   # Sicherheitscheck: Warnen, wenn nicht alle Ortsteile zugeordnet
-  if (nrow(stadtteildaten_df) != nrow(stadtteile_df)) teams_warnung("Nicht alle Stadtteile zugeordnet")
-  if (nrow(stimmbezirke_df) != length(unique(stimmbezirke_df$nr))) teams_warnung("Nicht alle Stimmbezirke zugeordnet")
+  if (nrow(stadtteildaten_df) != nrow(stadtteile_df)) teams_warning("Nicht alle Stadtteile zugeordnet")
+  if (nrow(stimmbezirke_df) != length(unique(stimmbezirke_df$nr))) teams_warning("Nicht alle Stimmbezirke zugeordnet")
   cat("Stadtteildaten aggregiert.\n")
   return(stadtteildaten_df)
 }
@@ -237,7 +240,9 @@ berechne_kand_tabelle <- function(stimmbezirksdaten_df = stimmbezirksdaten_df) {
     left_join(kandidaten_df %>%  select(Nummer, Name, Parteikürzel, Farbwert), 
               by="Nummer") %>% 
     mutate(name = paste0(Name," (",Parteikürzel,")")) %>% 
-    mutate(Prozent = Stimmen / gueltig * 100) %>% 
+    mutate(Prozent = ifelse(gueltig > 0, 
+                            Stimmen / gueltig * 100,
+                            0)) %>% 
     select(Nummer, `Kandidat/in` = name, Parteikürzel, Stimmen, Prozent)
   cat("Gesamttabelle alle Kandidaten berechnet.\n")
   return(kand_tabelle_df)
@@ -355,8 +360,10 @@ hole_wahldaten <- function() {
     }
     # Stadtteil neu ausgezählt?
   }
-  meldung_s <- paste0(meldung_s,"<br><br>",
+  if (!exists("NO_SOCIAL")) {
+    meldung_s <- paste0(meldung_s,"<br><br>",
                       generiere_socialmedia())
+  }
   teams_meldung(meldung_s,title=wahl_name)
   
   check = tryCatch(
diff --git a/R/lies_konfiguration.R b/R/lies_konfiguration.R
index b36523a..4c4d629 100644
--- a/R/lies_konfiguration.R
+++ b/R/lies_konfiguration.R
@@ -25,10 +25,20 @@
 # - wahlberechtigt - Zahl der Wahlberechtigen (kommt Sonntag)
 # - briefwahl - Zahl der Briefwahlstimmen (kommt Sonntag)
 
+# Falls der Parameter wahl_name noch nicht definiert ist, 
+# setze ihn erst mal auf das derzeitige Verzeichnis. 
+if (exists("wahl_name")) {
+  index_pfad = paste0("index/",wahl_name,"/")
+} else {
+  index_pfad = paste0("index/")
+}
+
+# Lies die Indexdatei aus dem Verzeichnis wahl_name. 
+# Falls keines angegeben: aus dem aktuellen Verzeichnis
 if (TEST) {
-  config_df <- read_csv("index/config_test.csv")
+  config_df <- read_csv(paste0(index_pfad,"config_test.csv"))
 } else {
-  config_df <- read_csv("index/config.csv")
+  config_df <- read_csv(paste0(index_pfad,"config.csv"))
 }
 for (i in c(1:nrow(config_df))) {
   # Erzeuge neue Variablen mit den Namen und Werten aus der CSV
diff --git a/R/main.R b/R/main.R
index 695ed73..a4cb93c 100644
--- a/R/main.R
+++ b/R/main.R
@@ -16,7 +16,7 @@ p_load(R.utils)
 rm(list=ls())
 
 TEST = FALSE
-DO_PREPARE_MAPS = TRUE
+DO_PREPARE_MAPS = FALSE
 
 
 
@@ -85,7 +85,7 @@ if (DO_PREPARE_MAPS) {
 while (gezaehlt < stimmbezirke_n) {
   check = tryCatch(
     { # Zeitstempel der Daten holen
-      ts_daten <- check_for_timestamp(stimmbezirke_url)
+      ts_daten <- check_for_timestamp(stimmbezirke_url) + hours(1)
     },
     warning = function(w) {teams_warning(w,title=paste0(wahl_name,": CURL-Polling"))},
     error = function(e) {teams_warning(e,title=paste0(wahl_name,": CURL-Polling"))}
@@ -107,6 +107,7 @@ dw_publish_chart(top_id)
 
 # Logging beenden
 if (!TEST) {
+  cat("OK: FERTIG - alle Stimmbezirke ausgezählt: ",as.character(ts),"\n")
   sink()
   sink(type="message")
   file.rename("obwahl.log","obwahl_success.log")
diff --git a/R/main_oneshot.R b/R/main_oneshot.R
index d4914a8..b23dd48 100644
--- a/R/main_oneshot.R
+++ b/R/main_oneshot.R
@@ -11,19 +11,41 @@ p_load(DatawRappr)
 p_load(curl)
 p_load(magick)
 p_load(openxlsx)
+p_load(R.utils)
 
 rm(list=ls())
 
-TEST = TRUE
-DO_PREPARE_MAPS = FALSE
-
-
-
 # Aktuelles Verzeichnis als workdir
 setwd(this.path::this.dir())
 # Aus dem R-Verzeichnis eine Ebene rauf
 setwd("..")
 
+# Lies Kommandozeilen-Parameter: 
+# (Erweiterte Funktion aus dem R.utils-Paket)
+args = R.utils::commandArgs(asValues = TRUE)
+if (length(args)!=0) { 
+  if (any(c("h","help","HELP") %in% names(args))) {
+    cat("Parameter: \n",
+        "--TEST schaltet Testbetrieb ein\n",
+        "--DO_PREPARE_MAPS schaltet Generierung der Switcher ein\n",
+        "wahl_name=<name> holt Index-Dateien aus dem Verzeichnis ./index/<name>\n\n")
+  }
+  TEST <- "TEST" %in% names(args)
+  DO_PREPARE_MAPS <- "DO_PREPARE_MAPS" %in% names(args)
+  if ("wahl_name" %in% names(args)) {
+    wahl_name <- args[["wahl_name"]]
+    if (!dir.exists(paste0("index/",wahl_name))) stop("Kein Index-Verzeichnis für ",wahl_name)
+  }
+} 
+
+# Defaults
+if (!exists("wahl_name")) wahl_name = "obwahl_ffm_stichwahl_2023"
+if (!exists("TEST")) TEST = FALSE
+if (!exists("DO_PREPARE_MAPS")) DO_PREPARE_MAPS = FALSE
+NO_SOCIAL = TRUE
+
+
+
 # Logfile anlegen, wenn kein Test
 # if (!TEST) {
 #   logfile = file("obwahl.log")
@@ -92,4 +114,4 @@ ts <- ts_daten
 
 hole_wahldaten()
 
-# EOF
\ No newline at end of file
+# EOF
diff --git a/R/messaging.R b/R/messaging.R
index 77807ca..80d8784 100644
--- a/R/messaging.R
+++ b/R/messaging.R
@@ -22,6 +22,7 @@ if (Sys.getenv("WEBHOOK_OBWAHL") == "") {
 
 teams_meldung <- function(...,title="OB-Wahl-Update") {
   cc <- teamr::connector_card$new(hookurl = t_txt)
+  if (TEST) {title <- paste0("TEST: ",title) }
   cc$title(paste0(title," - ",lubridate::with_tz(lubridate::now(),
                                                  "Europe/Berlin")))
   alert_str <- paste0(...)
@@ -32,13 +33,13 @@ teams_meldung <- function(...,title="OB-Wahl-Update") {
 
 teams_error <- function(...) {
   alert_str <- paste0(...)
-  teams_meldung(title="OB-Wahl: FEHLER: ", ...)
+  teams_meldung("***FEHLER: ",...)
   stop(alert_str)
 } 
 
 teams_warning <- function(...) {
   alert_str <- paste0(...)
-  teams_meldung("OB-Wahl: WARNUNG: ",...)
+  teams_meldung("***WARNUNG: ",...)
   warning(alert_str)
 } 
 
diff --git a/README.md b/README.md
index 32998ac..aae3d03 100644
--- a/README.md
+++ b/README.md
@@ -33,6 +33,18 @@ Grafiken:
 * Tabelle nach Kandidaten (3 beste, 3 schlechteste Stadtteile)
 * Tabelle alle Ergebnisse nach Stadtteil 
 
+### Aktualisierung via CORS/GBucket
+
+Der normale Weg, eine Datawrapper-Grafik anzuzeigen, ist: pushe die Daten auf den Datawrapper-Server - mit dw_data_to_chart() - und aktualisiere. 
+
+Alternativ kann die Grafik aus live bereitgestellten Daten in einem Google Bucket bestückt werden. Die Adressen der Dateien, die an die Grafiken übergeben werden müssen, sind: 
+
+- https://d.data.gcp.cloud.hr.de/obwahl_top.csv
+- https://d.data.gcp.cloud.hr.de/obwahl_kand_tabelle.csv
+- https://d.data.gcp.cloud.hr.de/obwahl_ergaenzt.csv
+- https://d.data.gcp.cloud.hr.de/obwahl_hochburgen.csv
+- https://d.data.gcp.cloud.hr.de/obwahl_stadtteile.csv
+
 ### Konfiguration
 
 Das Programm holt sich seine Daten aus einer Konfigurationsdatei - entweder für den Live- oder den Testbetrieb, was über die Variable TEST im Progammcode umgestellt wird. Die Indizes für die jeweile Wahl - Kandidatinnen und Kandidaten, Stadtteile und Wahllokal-Zuordnungen - liegen in einem Unterordner mit dem Namen der Wahl, als CSV oder XLSX. 
@@ -86,7 +98,7 @@ Spalte | Wert
 ---- | ----
 nr | ID des Stimmbezirks
 ortsteilnr | ID des Stadtteils
-ortsteil Name des Stadtteils
+ortsteil | Name des Stadtteils
 
 Nicht benötigte Spalten können in der Tabelle bleiben, sollten aber möglichst nicht "name" oder so heißen. 
 
@@ -107,11 +119,28 @@ Aggregation auf Stadtebene
 
 (siehe ["Sitemap"](./sitemap.md) für den Code)
 
+## Vorbereitung einer Wahl
+
+- Shapefile für die Stadt besorgen; Stimmbezirksebene; Stadtteile
+- Stadtteile aggregieren, GEOJSON generieren
+- Ordner für die Wahl im Index-Ordner; Datei Kandidaten, Stadtteile (mit Geokoordinaten für die Zentrumspunkte), Stimmbezirke (mit Zuordnung Stadtteil)
+- Kopien für die vier Grafiken anlegen: Top, alle Stimmen, Hochburgen, Stadtteile. Link zum Wahlamt nicht vergessen.
+- Leerdatei Ergebnisse nach Stadtteil vorbereiten
+- Kopie der Sieger-Karte mit GEOJSON anlegen; GEOJSON hochladen, Leerdatei hochladen. Link zum Wahlamt korrigieren. 
+- Eine erste Kopie der Choropleth-Karten nach Kandidat: Wahlamt-Link ändern und Karte und Leerdatei hochladen, dann kopieren. 
+- Kopien für alle Kandidierenden anlegen. Jeweils die Werte-Spalte des jeweiligen Kandidaten auswählen; benennen, um sie zuordnen zu können. (Farben und Namen werden automatisch nachgetragen.)
+- Indexdatei vorbereiten: Wahlname, Anzahl TOP, Dateinahmen der Index-Dateien, Datawrapper-IDs für die Karten und Diagramme
+
+## TODO
+
+- Analyse: Weshalb hängt das Polling manchmal hinterher?
+- Aufruf mit Parametern ermöglichen ("main.R obwahl_ffm_2023")
+- Oneshot-Variante für Kassel
+
+- Auswertung Briefwahldaten
 
 ## Nice-To-Have 
 
-- Vergleich letzte Kommunalwahl
 - Zusatzfeature: Briefwahlprognostik - wieviele Stimmen fehlen vermutlich noch?
 - Shapefiles KS, DA verbessern
-- Datensparsamere Alternativ-CURL-Poll-Datei (zB mit dem Gesamtergebnis)
-- Mehr Licht in den Choropleth-Karten farbabhängig
+- Vergleich letzte Kommunalwahl regulär
\ No newline at end of file
diff --git a/howto_shapefiles.md b/howto_shapefiles.md
index c75fe0a..9814772 100644
--- a/howto_shapefiles.md
+++ b/howto_shapefiles.md
@@ -8,6 +8,8 @@ Dazu Rechtsklick auf den Layer; Koordinatensystem WGS84, exportieren
 3. Stadtteile generieren
 Menü "Vektor", "Geometrieverarbeitungswerkzeuge", "Auflösen" - und dann in der Dialogbox auswählen "Felder auflösen [optional]", und dann die Attribute hinzufügen, nach denen zusammengeführt werden soll. 
 
+Nach den Attributen schauen - die Ortsteilnr. ist ein String, kein Integer! Rechtsklick auf den neuen Layer; Eigenschaften..., dann den "Felder..."-Editor, da oben auf das kleine Abakus-Symbol klicken, Namen für das neue Feld in Ausgabefeldname (z.B. "nr"), dann in Feld Ausdruck eintragen"to_int(Ortsbezirk)" - und OK klicken. Neues Feld wird angelegt. Dann das alte Feld löschen (auswählen, oben Klick auf Löschen-Feld).
+
 - Rechtsklick auf den Layer; Exportieren als GEOJSON - nicht vergessen, das Bezugssystem auf WGS84 umzustellen!
 - Rechtsklick auf den Layer; Export als XLSX - ggf. Geo-Attribute abschalten
 
@@ -22,4 +24,12 @@ Dann noch Geokoordinaten der Zentroidpunkte: Rechte Seite die Toolbox, dort "Vek
 5. CSV-/XLSX-Dateien putzen
 
 - Brauchen eine Stadtteil-Datei mit nr,name,lon,lat (erzeugt aus den Zentroiden)
-- Brauchen einen Wahlbezirks-Zuordnung
\ No newline at end of file
+- Brauchen einen Wahlbezirks-Zuordnung
+
+
+6. Reparatur der Darmstadt-Karte
+
+- Laden (falsche Geometrie - das erst zum Schluss fixen!)
+- Vereinfachen: Fläche
+- Auflösen
+- Löcher löschen
\ No newline at end of file
diff --git a/index/obwahl_ffm_2023/ffm_config.csv b/index/obwahl_ffm_2023/ffm_config.csv
new file mode 100644
index 0000000..2f97d66
--- /dev/null
+++ b/index/obwahl_ffm_2023/ffm_config.csv
@@ -0,0 +1,37 @@
+name,value,comment
+wahl_name,obwahl_ffm_2023,Welche Wahl?
+stimmbezirke_url,https://votemanager-ffm.ekom21cdn.de/2023-03-05/06412000/daten/opendata/Open-Data-06412000-OB-Wahl-Wahlbezirk.csv?ts=1677904123448,URL Daten-CSV Stimmbezirke
+wahlberechtigt,508182,Anzahl Wahlberechtigte lt. Wahlamt (kommt Sonntag)
+briefwahl,250000,Anzahl Briefwahlstimmen lt. Wahlamt (kommt Sonntag)
+kandidaten_fname,kandidaten.xlsx,"XLSX oder CSV, wird im Ordner <wahl_name> erwartet"
+datawrapper_fname,datawrapper.xlsx,"XLSX oder CSV, wird im Ordner <wahl_name> erwartet"
+zuordnung_fname,zuordnung_wahllokale.csv,"XLSX oder CSV, wird im Ordner <wahl_name> erwartet"
+stadtteile_fname,stadtteile.csv,"XLSX oder CSV, wird im Ordner <wahl_name> erwartet"
+startdatum,2023-03-05 16:00:00,Beginn der Auszählung
+top5_id,2DYBQ,
+karte_sieger_id,ANKmx,
+karte_kand1_id,RcvQp,Rottmann (Grüne)
+karte_kand2_id,jrm2v,Becker (CDU)
+karte_kand3_id,bKR8r,Josef (SPD)
+karte_kand4_id,etN3J,Mehler-Würzbach (Linke)
+karte_kand5_id,3mydT,Pürsün (FDP)
+karte_kand6_id,K3aCw,Lobenstein (AfD)
+karte_kand7_id,vtG4Y,Pfeiffer (BFF)
+karte_kand8_id,tRHeI,Tanczos (PARTEI)
+karte_kand9_id,v4Y5m,Schwichtenberg (Gartenpartei)
+karte_kand10_id,g3iBN,Wirth (unabh.)
+karte_kand11_id,4LxcN,Camara (FPF)
+karte_kand12_id,RZDF7,Pauli (unabh.)
+karte_kand13_id,F86gf,Junghans (unabh.)
+karte_kand14_id,bLPXL,Xu (unabh.)
+karte_kand15_id,Ktufa,Wolff (unabh.)
+karte_kand16_id,MO41j,Akhtar (Todenhöfer)
+karte_kand17_id,ccrfL,Großenbach (Basis)
+karte_kand18_id,q2S6m,Pawelski (unabh.)
+karte_kand19_id,697CL,Schulte (unabh.)
+karte_kand20_id,3lMmu,Eulig (unabh.)
+tabelle_alle_id,7kRPR,
+hochburgen_id,oB3KH,
+tabelle_stadtteile_id,LiXnz,
+social1_id,2DYBQ,5 stärkste
+social2_id,S9BbQ,Alle Stimmen angepasst
diff --git a/index/obwahl_ffm_2023/ffm_config_test.csv b/index/obwahl_ffm_2023/ffm_config_test.csv
new file mode 100644
index 0000000..07d5d1a
--- /dev/null
+++ b/index/obwahl_ffm_2023/ffm_config_test.csv
@@ -0,0 +1,37 @@
+name,value,comment
+wahl_name,obwahl_ffm_2023,Welche Wahl?
+stimmbezirke_url,testdaten/dummy.csv,URL Daten-CSV Stimmbezirke
+wahlberechtigt,508182,Anzahl Wahlberechtigte lt. Wahlamt (kommt Sonntag)
+briefwahl,250000,Anzahl Briefwahlstimmen lt. Wahlamt (kommt Sonntag)
+kandidaten_fname,kandidaten.xlsx,"XLSX oder CSV, wird im Ordner <wahl_name> erwartet"
+datawrapper_fname,datawrapper.xlsx,"XLSX oder CSV, wird im Ordner <wahl_name> erwartet"
+zuordnung_fname,zuordnung_wahllokale.csv,"XLSX oder CSV, wird im Ordner <wahl_name> erwartet"
+stadtteile_fname,stadtteile.csv,"XLSX oder CSV, wird im Ordner <wahl_name> erwartet"
+startdatum,2023-01-01 18:00:00 CET,Beginn der Auszählung
+top5_id,028Fp,
+karte_sieger_id,7gscI,
+karte_kand1_id,hM9SE,Rottmann (Grüne)
+karte_kand2_id,hM9SE,Becker (CDU)
+karte_kand3_id,07CR4,Josef (SPD)
+karte_kand4_id,07CR4,Mehler-Würzbach (Linke)
+karte_kand5_id,07CR4,Pürsün (FDP)
+karte_kand6_id,07CR4,Lobenstein (AfD)
+karte_kand7_id,07CR4,Pfeiffer (BFF)
+karte_kand8_id,07CR4,Tanczos (PARTEI)
+karte_kand9_id,07CR4,Schwichtenberg (Gartenpartei)
+karte_kand10_id,07CR4,Wirth (unabh.)
+karte_kand11_id,07CR4,Camara (FPF)
+karte_kand12_id,07CR4,Pauli (unabh.)
+karte_kand13_id,07CR4,Junghans (unabh.)
+karte_kand14_id,07CR4,Xu (unabh.)
+karte_kand15_id,07CR4,Wolff (unabh.)
+karte_kand16_id,07CR4,Akhtar (Todenhöfer)
+karte_kand17_id,07CR4,Großenbach (Basis)
+karte_kand18_id,07CR4,Pawelski (unabh.)
+karte_kand19_id,07CR4,Schulte (unabh.)
+karte_kand20_id,07CR4,Eulig (unabh.)
+tabelle_alle_id,PLwHI,
+hochburgen_id,Im2PX,
+tabelle_stadtteile_id,BM8kD,
+social1_id,028Fp,5 stärkste
+social2_id,S9BbQ,Alle Stimmen angepasst
diff --git a/index/obwahl_ks_2023/config.csv b/index/obwahl_ks_2023/config.csv
new file mode 100644
index 0000000..cf31ec1
--- /dev/null
+++ b/index/obwahl_ks_2023/config.csv
@@ -0,0 +1,23 @@
+name,value,comment
+wahl_name,obwahl_ks_2023,Welche Wahl?
+stimmbezirke_url,https://votemanager-ks.ekom21cdn.de/2023-03-12/06611000/daten/opendata/Open-Data-06611000-Direktwahl-zur-Oberbuergermeisterin-zum-Oberbuergermeister-Wahlbezirk.csv?ts=1678486050153,URL Daten-CSV Stimmbezirke
+wahlberechtigt,147463,Anzahl Wahlberechtigte lt. Wahlamt (kommt Sonntag)
+briefwahl,39092,Anzahl Briefwahlstimmen lt. Wahlamt (kommt Sonntag)
+top,6,Anzahl der Top-Kandidaten in den Darstellungen
+kandidaten_fname,kandidaten.xlsx,"XLSX oder CSV, wird im Ordner <wahl_name> erwartet"
+zuordnung_fname,wahlbezirke.xlsx,"XLSX oder CSV, wird im Ordner <wahl_name> erwartet"
+stadtteile_fname,ks-stadtteile.csv,"XLSX oder CSV, wird im Ordner <wahl_name> erwartet"
+startdatum,2023-03-12 16:00:00,Beginn der Auszählung
+top_id,Ts1oS,
+karte_sieger_id,O9wPT,
+karte_kand1_id,hM9SE,Schöller
+karte_kand2_id,07CR4,Carqueville
+karte_kand3_id,whgzp,Kühne-Hörmann
+karte_kand4_id,5CpYu,Bock
+karte_kand5_id,pc6vH,Käufler
+karte_kand6_id,sEJhl,Geselle
+tabelle_alle_id,EQ4dd,
+hochburgen_id,GMTSJ,
+tabelle_stadtteile_id,gJNPD,
+social1_id,Ts1oS,5 stärkste
+social2_id,S9BbQ,Alle Stimmen angepasst
diff --git a/index/obwahl_ks_2023/config_test.csv b/index/obwahl_ks_2023/config_test.csv
new file mode 100644
index 0000000..8c2e436
--- /dev/null
+++ b/index/obwahl_ks_2023/config_test.csv
@@ -0,0 +1,23 @@
+name,value,comment
+wahl_name,obwahl_ks_2023,Welche Wahl?
+stimmbezirke_url,https://www.eggers-elektronik.de/files/test.csv,URL Daten-CSV Stimmbezirke
+wahlberechtigt,147463,Anzahl Wahlberechtigte lt. Wahlamt (kommt Sonntag)
+briefwahl,39092,Anzahl Briefwahlstimmen lt. Wahlamt (kommt Sonntag)
+kandidaten_fname,kandidaten.xlsx,"XLSX oder CSV, wird im Ordner <wahl_name> erwartet"
+zuordnung_fname,wahlbezirke.xlsx,"XLSX oder CSV, wird im Ordner <wahl_name> erwartet"
+stadtteile_fname,ks-stadtteile.csv,"XLSX oder CSV, wird im Ordner <wahl_name> erwartet"
+startdatum,2023-01-01 18:00:00 CET,Beginn der Auszählung
+top,6,
+top_id,028Fp,
+karte_sieger_id,7gscI,
+karte_kand1_id,hM9SE,Schöller
+karte_kand2_id,07CR4,Carqueville
+karte_kand3_id,whgzp,Kühne-Hörmann
+karte_kand4_id,5CpYu,Bock
+karte_kand5_id,pc6vH,Käufler
+karte_kand6_id,sEJhl,Geselle
+tabelle_alle_id,PLwHI,
+hochburgen_id,Im2PX,
+tabelle_stadtteile_id,BM8kD,
+social1_id,028Fp,5 stärkste
+social2_id,S9BbQ,Alle Stimmen angepasst
diff --git a/index/obwahl_ks_2023/kandidaten.xlsx b/index/obwahl_ks_2023/kandidaten.xlsx
index e10b674797487ee43e2033972e604fe8f5e8c83b..f1c1eac6703306255f182e4ea891e49f72ddc201 100644
GIT binary patch
delta 2843
zcmZ8j2{_c<8Xrq`#V`hA31iE?^&zrv*=A&46OplvEm{B8>;@C!L-kn)O&E!66=@L3
zPHODRPT3N!bf0^>_dMr!-uJxcJ?Fg7^Pc5*AiX2ah%ls}WChXC(10!_d(|?+$tX@G
zL7q&G{7gYnoTE5X{HdU4L)cVmb>L%O{nz_}R|vkz5-~yz3q9mn;YW_$buw9>z6RbY
zNT77$#`mi#FDg$RG?&Z!rN=?<-=ZJmWpGGvWlr(ftwFbwydB@$moq7*_`Q~`Lq9Z_
zX|p?4P?I^fUZrI|4<{zaH$(Y>xe%RtVNppcabb??Rd&<bD8u7vh?20PCE$CF<pN^@
zi_{zWfL;dQ{f+q>&D?23g_QH6-P(C^SW5awPOBb{<`YG;q#kHMc#N6YRmSA8g=>ZH
zdF>9&IQ=YZ3fZ;AM}}7~J#DE<QGTfSgJ2X1D&@hch?)x;7L#zYZ+>oNwBMUj5(zS8
zxm0BFYTX~d4J`Dh3&m`{7YFWXeV!?~Sd%kHT_-pjh97;yeMlFLny~9vJz88)DMRyQ
z<#lxX%(U&1!qr`9k%@qa5+rTRq}IKhkx2#KSh0<prZ$;_<ufPJMGn03U9ETP)?=aw
zpS0i4jY3&O4JD`|TKKLY%USm$4Z&bq@L#0hFeX~Bx(JgE8#CcVtO4ysz%&Dpy;BJ7
z8uX51L3HmWq^(FTueq5ohr~Xd(C8+w(z4EeKC0@1k%fOdKVsn888W$u_K)%}T>92a
zuPKJ6N~cuSM1?eemgBSjVSKH4-GvW>AwaWm9mq-I^<Ic=d8Ju_r;1Z<T)}Ml)unX#
z(6??yBU0Pdw%_j0+p#|eoTN2B<=N^#(ffqE@npAAC1r7?rw<mC`)ciRA4S)JhD&WO
zwJMEnG_Mf?VMm|(GgBX2lS;MO{ezqe&p%mL{e6*9DT{J586ih?<$lhlB#rpebcg0q
zPP!4=QUCo)$%n^LKNJXqBVn9x;hvu?*Yak|<LQ@6Sk?9Omu#9ifuA;I8gs7@qTf$^
zIKD{4=13fM-ze)J6HAd$tEhe1tK=@6Am-KNhcj#)v+<ABPC9<WA>w8kf_OG%@)@QR
z0+pO1|L5%jdYxNPBh7k(<^){Z!2AX5=PjU{n>e7GD#Z+gG28QB9Zxh!+f*mnx(*YF
z6&m86SaiJj@^vj&045`+-N0;hk0?m4fEiR=Gp6NG;EQZdEH2XFvf<~Hv`h0U=gbJA
z??lg@HZ?aD>w@H48aXDE?>z9EBBZ+*>S;xfj9b18bfNF8KgIWAMvXFFyNleB456bk
zHG%S8b#z|qzbyX+MK7kJ1A{FJDzFBWIj5{xnpK7{5=_6rveL$r7g<k8RsN~CX7d(T
zI@dYihYI-XXq;T;4}9I6t9Ivh&(E`hcu~F1Ye;gJZe7m%n%zCj5NG9HR=R5S!{ud(
z<OyzNuPNYm)^q;H>GEGz^yyDNKm0o)rJaK|sHD3+x(S#k?Fry8;>Q&!b|ll#W8$ov
zt|s8>ab<a1mt<;vJU;8ItI6%L%^SS6aoF8;LHyVaKAb6Z@~BrWLJhgk@@u7|q&L7|
zDL^WmddTTDrY!4Y?t1fdj-~usWyMC(vYqira@_H|S+$i}QgHds6H8AWO%kdh((1WR
zL@;3Ix;y$TU7qWfv!?3~NocX|pSD9O+2%D{y?lrbH-`<kjh=0P#hz5syY!aPOg9}N
zA#GwMO_0TKFt485nB$5*fxn6%P(>)l_ox3@UnQ>2d7f_Vx)b`lKHaiF_j?HW1So(5
zcPV>q7DIZzql={TG9{RItVU9Y0B8<vLA8Mpjc<5u1l)dr=(yPvGU5J?SH!EJLTMN+
zU1&_9TA=Jims92J8CAA(p~4|ubkcGcwI5_s<6<O_Zw<8!CKhrn#rb}vN1m8LLOBJ6
z4N>>3`h>sVQq4ef=gsC+Mclm8v%U*&jz`VJDaGF$$q1TROr=l9fnLx8={zL&EsQE;
z$9f=1$;W7#KMU@e)8paImvEaS^Ke^}_+czpD^w~tw)Mifi3iT#MDPK036k`<{%Nix
zYZHTozs;Vc4HA>S`cN<3r_DYwBG8e#{O+oRg)6!H+4pniK1JuZJ5-ULM)Ewk8>V;N
zOwozheb8lAaMWsn_2bFatfKoOu(NbDb!912)bKL!z~4~aS<fNdA}Oe$X-pg<xl;Rc
zTMc1IP2(6PcZi?@fhKuDp#S-8uma~rfk|nmu8jyfG5cTxkax5N5yfYdtj;`Zx?v*A
z$ChO5ExfiZuDZ(+I-3JssmZQfE9K0;-gS`n<Y2pR9Qa|GT}|d4m&H;(w!=jwYmt|c
zkVGK0PGPv*#B({Mvg4Az(RGE;UES}pzK0t$DtJ9PRJ@X3AC>$!UOBo`s%a1cJZE2y
z^)%tU>qZB%%l09CyZQnG9dyQ3mKG|;KJGw?whc=k5}5XNDUUz$VHP{DtH6R~9>LHt
zjrdCgZJrdQ)|)5lYGM*0=iryiVm$E@i7sIu+(mqe7avR63p;7pfm=w``t}vgULT|2
zuRhiz5INfMOD|gW^L>VP!??PD6bV?EPh;YEcq~2AsAr8q&tTG!I3melir2{iyV(~X
za%wC*U&-jg5vFfZWh<k6-aOMA_Z6#QLZ$d6wcff-?%Lt_x!1sJ7!7{zB^4icXu4*)
zYH1MgR(xuxQIp=qFq6%CC5&troX`ghZ?vk`<Dr$5o*w*07NPKHF+Knz57_s$+IIFP
zTH0hWw#-oUA=&&*4w1dPdWF}U?;f>G=%m2hIC}kl*18u))dYJ{csOsFO>@N-zN8nF
zK!vrrAVtNAT#Px<_yscRh%T^C+(EM#o0)aAh89bs9QU4H><ptWtM<s^z9u)tvw4FL
zr9mYbcdg)2SXHt<A2FrUxgFlDt!7&XR^|P3zs^hM<)?SflY>Cb)c=280-4}|gK4ZZ
zYs8*aMw^ysrw2`q_EM4kO@7@uJMRr^$hDz6E{A(;>|V=Y(t6X<_o=64-y$4Bt_<kP
z%WA=CGO4o^?1{2kQf~&^b&6`u{cRP%vVox3#RH2T^Vgh=rZ13>o`B|*1U$fHvh}r=
zg6yz{==}Vbq9p=jX?R=^BG(!~?&=u6M6&mz8FTzmqU#J13QGSJ3%X-VCzmV#nr6^C
zr=agCO-pmtL*mP%efWT!dynmSY)r~vX2EcnfROXY1`XJr*_hfZ->SDuk>*ZB9}5OV
z5f9AZ0Tn7btp6fMPue@r@OF_L5zK3m-3}sVHf9PAH^IAah#v2>$#DU*x=Y+)<dTaw
z9<cSifSsSm#C1Tj7e*yimZqh6Lc8xTo7f;-tk%DHlKottR*{_VN?&$aCuQDzsC}-<
zc*-1k_wwNF0jtp(mQSQ!gK?`u^jO10WH>pY!-xf43u%bqQ@saVbv-`ih14{Ydzks#
zWFXMgX*T|SbE$q4MFKY%`I`><v5cJYLj%^b--d0-<zEJ@7@P8$yv?R~CX3m0Xix2y
zXU_q4i%pgHSMSecvx5FC5Uep&;LPXGFqr>z0o}r4pcl`4%EfC|*-k@kJq6SM_%LAm
rp}c1iY(in=H(0PyERYKbLo#w!(0_O@R)!r$rie9W7os9Te$D<DNRJz*

delta 2822
zcmZ7&2{hDScNp6!Od2Dhu}mfzvL$0GYqI?9QDa{Q*+PY^KU=bEjD07JCBhJ89a%!i
zl2DN(+e9H-;y>wp-|73_x%b`W-FNRh=ic|;{UNy{$)>LjW`KcMSXe-mK&NUpBpsMW
z$+C1Z^oNWym=S!)+-BlFTq4FYD*?}CoJK?!y&a}uW(+#6ePTy6)_(Y(?(5R<CXGyZ
zp5hGfYEal(NL%)pN92rjU^p%HQ^d^y$-DfCSJ1^1v+civT79v1vQ<8!yC8ip+XDsf
z|FqfZ-s{*lsz#~FKNJvyuUJOzWzTaB<rFrYsLH}u)!1F^q?g!vbp3PI#~VPisLn-M
zKHVnR$SZM~rrFlSu&>^-LT}Lug~;RfoVuLo;Gk-fuYtXPJRoZI>N`ZMVq5l%WvZCh
zWx#b#+o=Ob@wSClUo#??H?37lECqa7*pl9%OY$pRE7VU$#|0Jj!q!Qu`|#WId81&R
zl6wY>jeXC050bIi43>69GH`<3_W?1q%`_g-lM=c5%#?eJD#lP|?~d8p$V^>NYxmV~
zU`I9H|2YKsgmrNiKmnuMXHP!KwjI>v6}YG2VK8Sz;nkfr%QzOv8uXgtPOWw5K|#R*
zljhqGf}M?nN4hWRH=V{BkYL+3+MJva&I{8%K2V5jO`s;?h9N4Fs0k4OS_Ka9RD~z8
zMekTpd5NnWyl<^W-}RS_uESfa!V(!x9#oxmuVC)V!E1csFC)H;TyN^1`3#y<I5(fl
z9d7dRC?CWxe00q_&|0NxBQr_;L$}H)gyR#W(St{|1X7Avt*&vA{M&mi5)e#h>uwfy
zc!59D*F>?0?A&jZ=x!4Z1hunxIXVk^$t>wIy3}>g;bg6@-<m3EJbH`6?UdvI$H|!H
ziwnw<tb5j>p_8W8pY_xdgY9L5^~SDCdT<S;*cV-J?;#+AuZL$yjpx^f`zTvum=W-$
z`n&khbrnBLR2)0w=#%v1%Bc}~5!~`q=Rug_Idgqf5lChD;<?^8z{f#pwv!wYTKNY)
zeM;^;wXTbLOfp_AmN?fP<v2#m0Sp#nZqpa1ZL!omkjRVF+Rz(Nk|OtpmCbptO`Yj&
z^q7G|FG)POvw8;N+|jGPof=ser<5A$!YlP$EnLnvAZmO(@*}6Gov_IHc5t8z!C+A6
z5mWVJop!F85<NBo;MVa9I-zdPBb3TAdQoe(_QGlWX!!P5p?E*ICqC+CgX{U4Y-go0
zap;Svx3npG=H=MXw@(iE<#C%X&q0NFaeg*N6FW1ykvMVaNIVS_<QUR}Ttn_|OD*8K
z+lGg#dd^lo%cN=2{)<P^iUsV7x_)Dxw!q>|4=Ww&+CPd<mHwda<DY)`-Jt>LrEjyf
zj{KyQ_~WYGrTrf^?2dH&g@rE?$T?nFP7apFRXi+?lOj2kjWWmL>mo)nP|NJ}lp3qW
z1UlmLJyPZL{6N0uRCKt)aNZMP@jZXw)~5Kr19Frga=`QjNCc)63NP1eKa#i=sJOTz
z9Fxfd`9&!DTGGcGsA#ie7t_iE@ha>A-gmaHqIeanwlfL(S2Om)Bn~!ysm<u2X`VJv
z@B_`%{Xn1FfOD0_^2-JTLKJNeZm71rpvAw;{k*Xl=dMi5!Q&WUI(w|%w1Nu~SU$09
z@d5th1K>uOFng;o?2&XILU;pYru9YduC%_SK6T-DGQoS{X~6G<c{J6El*X=^Ud`Z2
zPlF1U3ym5ZOb7C%TliN9_eVb-h^N7G1?*BIS(u#ggch3cX88{^cU`AoNa+oIZ!}&{
z97j8*&x?-C`nKcMpQ?>o8O!JbRyYxl8j*B?rth7b1P^6le~Q?=eXbNxSmjypMgIlm
zt{mHj$5Mdkjmn4$x{NW!Ja0_#O|yyenYS29ywp&`<`V9MpwI2B<1Ix`)P89jFE}M-
zp-Xuz&*#2Q{h`$Eg^$KoZbnUrHck_%>}B_le}P3CtB&2zl#!4xre6>N<o$NjIu$ps
zjoK-rzBEXM#AqZw#+@bA-RCQ?S6k^jB6!uxihVKcJ=^AG7~U?T|53w$DBp8*p+eXl
zq%G!jvb$EDoR%=ExRF20NaWhSO#j&c?(UiI)bDdyot_vM^A?KJeo#eF>&{kN$4~!^
z#P%H(-GK`!pQwWs8L{gPs^-Q@h*Cww{1hhPA6(5&f07wJD|YEqxz=NHf3riW2QFbQ
z^?*)ao0&z9ikdZG0)ZwGAkhDOH$)+JQ6N(i+O-<U8e{GQ)~wa3p*P8FD?W29dX3}*
zR;{8Jsbh0nM_=0H*-YX+H`ZfP_B7SmP&albu3~4~fqFTxTx%?JxF99x`~nsZP9IBu
z$UHc?YPo`fk3{svrq(eG?y^t%ttmE`@<x;z`kUCiS{!=nWvbNa)p|Lk<D75BS>TrB
zHD<G1*tdVULB(gk=FbMyib)i94nHS!k=5;D&Km|eS?@bieCob^s8>DpBZ_oSZlyrE
z&U(kw$T-jZ-F9VQ=YB{~W%6gPhvR9imCvLF;y+x;&TkS73vcmA4q&v`_!dMK@(fub
zyB$ena33k~7uPOmKk~)`sZ)>J5@rWJ-Rvul=T{RnbM+96)qK|c9jVC-c{ZuB&QQf-
z*q$YJ*GjBp&zt{s!!G(>y0S5X>%yl=VQM|s4Nx4Vp<`-TmagWz0OB)J&J`17=AEk2
z{)@E2)o48YUW#K=wo+du)3A_nY}Yclxx;21@dNA5QL%i*B2-HDn1h!eGDHjzOppc4
z`<tUb*=v-ed(;QEL%y5=V@SVB?6=50r4~&=Q`Gi_2wc}~K7filE%1u}os#K^CYXG;
zHR_v+W=vPs>Wv$$VCk!2_IW3L+mv&w!(H+ZR-!dng(Yt~(Qi47%;05PD3PHhlk`?4
zc6o%0qPw;6FwWIz29Fw4-Z)yEw2?KVDjj&eJWl0jE3H77SuTHF)#9KBff||rzhG`d
zkwBmB3rT3xIu~KJbUj$5LN+;XL+vy&ROx+evm0Br-NY@`gHtG{Vfy5U0pr;`gDZa7
z{VjMNYTFHiQqkzE$5@rBqmR_(6wKQmo62ibD3cc#V>GH|j&azz(j{w?9CFpjNm4hM
zt%*<*z9>2=Pl9x%xG5Q8*I4IzjO~%M4&uDaW6__5E^V5yxN3&07%$j`i2PH0LzF}^
z#n4@6;-p7JJ_Do|$CqFmFsdLqoYVYT8l@%r-tq;`NsoQ$oEVdh#oI>;ZbdBzCt4*C
zIg<ve`57eJ;5F)N78|A#@Tq&?;pFA?)Qww~X#E^D&6DbyX@yq;bUr1S$4yJ`EdxPz
z<DJPpyqjs;X<_#2H|KrE35uZH)N2*>`;<HEGkYz`NA=0QiKg`%+E$Q{_5Q1#Ov~6r
zX6iIyFCF$h%{*Q+DJyoAw>FuLs<ythrfGIpBeU}B<}YyCvieC*1a?Mi;p64Kb;P>+
z?_9v^8N{9GK%jTDd>me0mj5)1WFAg~KP{o#@i&J(;uTo&AH@on+;0UD1y?wvc)0u_
z&4piq(DWvUF9Pu^{F1<L>F;#IKz|Q7J5h=qPPFDedT99fS&sc}2XcfGL!ifrx!gw&
o<5Y6<(m{#6+=xSoN|W$JmJ>pUlK2T;x{E~36CzBrT)%z%3)VO`!vFvP

diff --git a/index/obwahl_ks_2023/parteien-kommunalwahl.xlsx b/index/obwahl_ks_2023/parteien-kommunalwahl.xlsx
new file mode 100644
index 0000000000000000000000000000000000000000..5b382a6653d2514848c5bbbe8651c2b62bdb5d04
GIT binary patch
literal 6155
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
SBqSX85e%o8XK=X&3F%*>WQLyr

literal 0
HcmV?d00001

-- 
GitLab