Dobra mapa trasy powinna odpowiadać na pytanie, które pojawia się zaraz po wyborze drogi: jak dotrę na start i jak wrócę do domu? Na mapie tras Ekstremalnej Drogi Krzyżowej często oznacza to znalezienie najbliższej stacji kolejowej, sprawdzenie rozkładu i ocenę, czy ostatnie połączenie daje bezpieczny zapas po nocnym przejściu.
Chciałem dodać te informacje bez ręcznego utrzymywania prywatnego spisu stacji. W praktyce zadanie połączyło ekstrakcję z OpenStreetMap, współdzieloną publiczną usługę zapytań, kilka rodzin identyfikatorów i linki do systemów, nad którymi nie mam kontroli. Narysowanie znacznika było najłatwiejsze. Trudniejsze okazało się określenie, co ten znacznik reprezentuje i jak bardzo można ufać każdemu linkowi.
Jedno zapytanie dla całej Polski było złą jednostką pracy
Pierwsza wersja wysyłała do Overpass zapytanie o wszystkie prawdopodobne stacje kolejowe w Polsce. Składnia była poprawna, lecz poprawne zapytanie dla całego kraju nadal może być niewłaściwym obciążeniem dla współdzielonej usługi. Kończyło się przekroczeniem czasu albo odrzuceniem pod obciążeniem. Ponowienie tej samej kosztownej operacji tylko powtarzało błąd.
Dokumentacja publicznych instancji Overpass opisuje ograniczenie wprost: serwery dzielą skończone zasoby między wielu użytkowników, stosują limity i mogą odrzucać obciążenie. Overpass QL pozwala precyzyjnie ograniczyć obszar, ale nie sprawia, że nieograniczona ekstrakcja staje się tania.
Podzieliłem więc pracę na kafle 1 na 1 stopień. Dla każdego kafla przecinającego Polskę wysyłałem to samo ograniczone przestrzennie zapytanie, zapisywałem surową odpowiedź w pamięci podręcznej i rejestrowałem współrzędne kafla, numer próby, status odpowiedzi, czas oraz liczbę elementów.
[out:json][timeout:60];
(
nwr["railway"~"^(station|halt)$"](49.0,19.0,50.0,20.0);
nwr["public_transport"="station"]["train"="yes"](49.0,19.0,50.0,20.0);
);
out center;
To przykład jednego kafla, a nie pełna pętla dla kraju. Proces produkcyjny generuje siatkę, pomija kafle poza granicami i ogranicza współbieżność. Nieudane próby trafiają do kolejki z wykładniczo rosnącym opóźnieniem i losowym rozproszeniem. Udana odpowiedź jest używana ponownie do czasu zmiany migawki, więc restart nie obciąża niepotrzebnie Overpass i nie wyrzuca wykonanej pracy.
Kafle tworzą nowy obowiązek: obiekt przy granicy może wystąpić w kilku odpowiedziach. Najpierw usuwam duplikaty po stabilnej parze OpenStreetMap (type, id), nigdy po nazwie. Węzeł, linia i relacja mogą mieć ten sam numer, a nadal być odrębnymi obiektami, dlatego typ jest częścią klucza.
Obiekt OSM nie jest automatycznie fizyczną stacją
Eksport projektu zawierał 6205 obiektów OpenStreetMap. To pomiar z tej ekstrakcji, a nie oficjalna liczba czynnych stacji kolejowych w Polsce.
Różnica jest istotna. OpenStreetMap może opisać miejsce kolejowe węzłem lub obszarem, a relacja transportu publicznego może grupować miejsca zatrzymania, perony, wejścia i obiekty stacji. Dokumentacja railway=station rozróżnia także railway=halt i schemat transportu publicznego. W istniejących danych spotkamy węzły, linie i relacje z railway=station, railway=halt albo public_transport=station, czasem opisujące nakładające się części tego samego miejsca.
Zachowałem zatem dwa poziomy:
- Warstwa obiektów źródłowych przechowuje typ i ID OSM, tagi, współrzędne lub środek, pochodzenie kafla i czas ekstrakcji.
- Warstwa miejsc aplikacji grupuje prawdopodobne reprezentacje jednego punktu pasażerskiego i przechowuje pewność dopasowania.
Pierwsza warstwa jest odtwarzalna. Druga wynika z reguł produktu i może zostać przeliczona, gdy poprawię resolver. Połączenie ich już podczas importu uniemożliwiłoby wyjaśnienie, czy zaskakujący znacznik pochodzi ze źródła, czy z mojej logiki.
Odrzucałem rekordy jednoznacznie niepasażerskie, jeśli tagi dostarczały takiego dowodu. Nie zakładałem jednak aktualnego ruchu tylko dlatego, że obiekt ma tag station. Może być zamknięty, towarowy, historyczny, błędnie oznaczony albo zbyt słabo opisany do automatycznej decyzji.
Link do Koleo wymaga normalizacji, nie prostego usunięcia spacji
Migawka Koleo użyta do dopasowania zawierała 5624 rekordy stacji. To również wynik projektu, a nie deklaracja o bieżącym oficjalnym spisie w serwisie Koleo. Strony stacji mają czytelne fragmenty ścieżek, dlatego potrzebowałem deterministycznej funkcji obsługującej polskie znaki, interpunkcję, wielokrotne separatory, pustą nazwę i wejście Unicode.
export function normalizeStationSlug(name: string, fallbackId: string | number): string {
const slug = name
.normalize("NFKD")
.replace(/\p{M}/gu, "")
.toLowerCase()
.replaceAll("ł", "l")
.replace(/['’]/gu, "")
.replace(/[^\p{L}\p{N}]+/gu, "-")
.replace(/^-+|-+$/gu, "");
const fallback = String(fallbackId)
.toLowerCase()
.replace(/[^\p{L}\p{N}]+/gu, "-")
.replace(/^-+|-+$/gu, "");
return slug || `station-${fallback || "unknown"}`;
}
export function koleoStationUrl(name: string, fallbackId: string | number): string {
const slug = normalizeStationSlug(name, fallbackId);
return new URL(`/dworzec-pkp/${slug}`, "https://koleo.pl/").toString();
}
Funkcja buduje kandydata na adres, a nie dowód istnienia strony. Sprawdzam wygenerowany link względem migawki, zapisuję rekord dostawcy uzasadniający połączenie i pozostawiam w interfejsie ścieżkę awaryjnego wyszukiwania. Po zmianie sluga przez dostawcę znacznik trasy powinien nadal pomagać, a nie prowadzić z pełnym przekonaniem do błędu 404.
Normalizacja nazwy jest tylko pierwszym etapem identyfikacji. Resolver porównuje znormalizowane nazwy oficjalne i alternatywne, odległość, typ stacji oraz dostępne tagi referencyjne. Identyczne nazwy w dużej odległości nie są dopasowaniem. Bliskie współrzędne z różnymi, ale wiarygodnymi nazwami trafiają do przeglądu. Przypadki niejednoznaczne pozostają bez połączenia, dopóki nie pojawi się mocniejszy dowód.
Identyfikator podobny do UIC jest sygnałem, a nie wynikiem działania matematycznego
Dane kolejowe zawierają kilka rodzin kodów miejsc i przedsiębiorstw. UIC ATLAS opisuje zarządzane kody stacji używane w operacjach i dokumentach transportowych, a dokumentacja UIC dotycząca kodów krajów mówi o kodach używanych w międzynarodowej wymianie danych.
W tym projekcie znany prefiks kraju był przydatnym sygnałem, gdy dwa rekordy zgadzały się już nazwą i położeniem. Nie był bezpiecznym przepisem na obliczenie brakującego identyfikatora stacji. Rodziny kodów mają różny zakres, istnieją formaty historyczne, zera wiodące są znaczące, a strukturalnie poprawna wartość nadal może wskazywać inne miejsce.
Proces dopasowania wzbogacił 2922 z 5624 rekordów Koleo o identyfikator Bilkom/HAFAS. Daje to 52,0 procent pokrycia względem migawki Koleo. Nie oznacza to, że 52,0 procent polskich stacji jest czynnych, kompletnych ani obsługiwanych przez każdy serwis rozkładowy.
Określenia Bilkom/HAFAS używam celowo. Bilkom jest publicznym serwisem rozkładowym w tym procesie, natomiast HAFAS to rodzina systemów informacji pasażerskiej. Firma HaCon została założona w 1984 roku, a jej obecne portfolio informacji i sprzedaży biletów opisuje produkty HAFAS. Ta historia nie oznacza, że dzisiejszy produkt HAFAS lub konkretny identyfikator działa niezmiennie od 1984 roku.
Dla każdego połączenia zapisuję dowód i metodę: dokładny identyfikator dostawcy, znormalizowaną nazwę z odległością, zgodny sygnał UIC albo ręczną decyzję. Przechowuję też daty migawek. Dzięki temu wzbogacenie można wyjaśnić i wygasić po zmianie danych dostawcy.
Współrzędne tworzą najtrwalszy link do Google Maps
Nazwa jest dobrą etykietą, lecz słabym globalnym lokalizatorem. Różne miejsca mogą nazywać się tak samo, nazwy się zmieniają, a interpunkcja bywa niespójna. Gdy miejsce stacji ma sprawdzone współrzędne, generuję link wyszukiwania Google Maps z szerokości i długości geograficznej.
export function googleMapsStationUrl(latitude: number, longitude: number): string {
if (
!Number.isFinite(latitude) ||
!Number.isFinite(longitude) ||
Math.abs(latitude) > 90 ||
Math.abs(longitude) > 180
) {
throw new RangeError("Invalid station coordinates");
}
const url = new URL("https://www.google.com/maps/search/");
url.searchParams.set("api", "1");
url.searchParams.set("query", `${latitude.toFixed(6)},${longitude.toFixed(6)}`);
return url.toString();
}
Oficjalna dokumentacja adresów Google Maps wymaga api=1, przyjmuje w parametrze query szerokość i długość oddzielone przecinkiem oraz zaleca standardowe kodowanie URL. Użycie URL i URLSearchParams eliminuje ręczne składanie znaków specjalnych. Sprawdzony Place ID byłby mocniejszym wskazaniem, ale współrzędne stanowią niezależną od nazwy ścieżkę awaryjną i nie wymagają zgadywania wizytówki.
Zachowuję pierwotną pozycję OSM obok obliczonego środka. Dla linii lub relacji out center jest wygodne do prezentacji, lecz nie musi wskazywać wejścia na peron. Przy małej precyzji interfejs powinien mówić o obszarze stacji, a nie obiecywać konkretnych drzwi.
Powtarzalną alternatywą dla kraju jest lokalny plik PBF
Zapytania kafelkowe dobrze sprawdzają się przy okazjonalnym odświeżeniu i łatwo je kontrolować. Przy częstych przebudowach danych dla całej Polski bardziej przewidywalne i uprzejme wobec publicznej usługi jest użycie lokalnego wycinka. Geofabrik publikuje wycinek Polski, a osmium tags-filter filtruje węzły, linie i relacje po tagach.
curl -L https://download.geofabrik.de/europe/poland-latest.osm.pbf \
-o poland-latest.osm.pbf
osmium tags-filter poland-latest.osm.pbf \
nwr/railway=station,halt \
nwr/public_transport=station \
-o railway-stations.osm.pbf
osmium export railway-stations.osm.pbf -o railway-stations.geojson
Lokalny filtr celowo tworzy szeroki zestaw kandydatów. Później stosuję te same reguły ruchu pasażerskiego, obsługi geometrii i identyfikacji co w ścieżce Overpass. Dzięki temu polecenie powłoki nie udaje, że samo rozwiązało problem duplikatów.
Migawka PBF daje również trwały skrót wejścia i pozwala uruchamiać testy regresji niezależnie od dostępności publicznego API. Nadal zapisuję czas pobrania i źródło. Dane OpenStreetMap zmieniają się bez przerwy, więc odtwarzalność oznacza znajomość migawki użytej do wyniku, a nie oczekiwanie identycznej listy w przyszłości.
Zasady produkcyjne ważniejsze od samego zapytania
Gotowy proces opiera się na kilku regułach, które okazały się ważniejsze niż pierwsza składnia Overpass:
- Ograniczaj każdą zdalną jednostkę pracy i współbieżność.
- Ponawiaj tylko błędy przejściowe, ze wzrostem opóźnienia i losowym rozproszeniem.
- Zapisuj surową odpowiedź przed transformacją.
- Zachowuj tożsamość źródła i pochodzenie przed łączeniem miejsc.
- Usuwaj duplikaty OSM po typie i ID, a fizyczne miejsca rozwiązuj osobno.
- Przechowuj pewność oraz dowód dopasowania, nie tylko wybrany klucz obcy.
- Sprawdzaj linki rozkładowe i przy niepewności przechodź do wyszukiwania.
- Mierz czas kafla, liczbę prób i elementów, trafienia pamięci podręcznej, pokrycie dopasowań oraz nierozwiązane grupy.
- Alarmuj przy dużej zmianie liczebności zamiast publikować ją po cichu.
Mapa podaje też źródło danych. OpenStreetMap jest udostępniane na licencji ODbL, a wytyczne atrybucji fundacji OpenStreetMap wyjaśniają sposób prezentacji informacji o autorstwie. Atrybucja jest częścią funkcji, nie zadaniem dla stopki odkładanym na koniec.
Efekt dla uczestnika jest prosty: pobliskie miejsca kolejowe, link do rozkładu tam, gdzie dopasowanie ma uzasadnienie, oraz link mapy oparty na współrzędnych. Pod tą prostą interakcją działa proces, który potrafi przyznać się do niepewności. To najważniejsza lekcja z wdrożenia: otwarte dane zapewniają znakomite elementy, ale jakość produkcyjna powstaje dzięki ograniczonej ekstrakcji, jawnym dowodom tożsamości i uczciwym ścieżkom awaryjnym.
