zip-codes-pl 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9f093d90a449c54edbdfe6298dd0d3d480ac666eaefb25daad255d9ce6f86877
4
+ data.tar.gz: 0b3ccc8540f608eb8f8c6cce891dc0d75fa62cf52e11ee2f166984d53760f7d1
5
+ SHA512:
6
+ metadata.gz: 07ebb11afbc3b7a95663a5fabc6e23946c31862e3441aa3d0ab2122e67ce4527c90c79f88549bb9a2ceb453d254c21720fbfcf0f5d56e34db65148ee068706ef
7
+ data.tar.gz: 89ffefe6ec7f2c9e41061b5960e71b5cfda91b2484aae3e92eeb0547a34ce0bb2a2583d8c9991519625711acedbaf8aa38bb9c5372055ba189fa25839e4d334b
data/LICENSE.txt ADDED
@@ -0,0 +1,27 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Łukasz Kerl
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ ---
24
+
25
+ The dataset this gem builds is derived from GeoNames (https://www.geonames.org),
26
+ licensed under Creative Commons Attribution 4.0. Redistributing a built dataset
27
+ requires crediting GeoNames.
data/README.md ADDED
@@ -0,0 +1,204 @@
1
+ # zip-codes-pl
2
+
3
+ Polish postal codes, places and voivodeships **with coordinates**, built and
4
+ refreshed by a single rake task.
5
+
6
+ *(Dokumentacja po polsku: [README.pl.md](README.pl.md))*
7
+
8
+ **The dataset ships with the gem, so there is nothing to build before first
9
+ use.** A rake task refreshes it into a directory of your choosing, asking the
10
+ server with `If-None-Match` so a repeated run downloads nothing when the source
11
+ has not changed. Your refreshed copy takes precedence over the bundled one.
12
+
13
+ The dataset holds **72,899 rows**, **20,299 postal codes** and **52,325 places**
14
+ across 16 voivodeships, as a 6.6 MB tab separated file. Lookups scan the file
15
+ without building an index or retaining the whole dataset in memory.
16
+
17
+ ## Why this exists
18
+
19
+ GeoNames is an excellent source of Polish postal codes and coordinates, and a
20
+ poor source of Polish *names*. It labels all sixteen voivodeships in English and
21
+ inconsistently - `Lower Silesia`, `Warmia-Masuria`, `Łódź Voivodeship` - which is
22
+ not something you can put in front of a Polish user.
23
+
24
+ This gem carries its own verified table of the sixteen voivodeships, mapped onto
25
+ the official **TERYT** codes used by Polish public administration, and replaces
26
+ the upstream labels with it. The TERYT county and commune codes that GeoNames
27
+ does carry are passed through, so the result joins cleanly against any other
28
+ Polish register.
29
+
30
+ ## Installation
31
+
32
+ ```ruby
33
+ gem "zip-codes-pl"
34
+ ```
35
+
36
+ ## Refreshing the dataset
37
+
38
+ GeoNames republishes daily. To pick up a newer extract than the one bundled with
39
+ your installed version:
40
+
41
+ ```bash
42
+ rake zip_codes:pl:update # into the default directory (data/)
43
+ rake zip_codes:pl:update[db/pna] # into a directory you choose
44
+ rake zip_codes:pl:info[db/pna] # what is built, and when
45
+ ```
46
+
47
+ When the GeoNames extract has changed, the build fetches county and commune
48
+ names by TERYT code from the public Poczta Polska search.
49
+ Requests are sequential and spaced by at least 0.25 seconds by default; a `429`
50
+ response delays the next request according to `Retry-After`. A full enrichment
51
+ currently takes about five minutes. Poczta Polska is not queried after a
52
+ `304 Not Modified` response from GeoNames.
53
+
54
+ In a Rails application a railtie loads the tasks for you. Elsewhere, add one
55
+ line to your `Rakefile`:
56
+
57
+ ```ruby
58
+ load Gem::Specification.find_by_name("zip-codes-pl").gem_dir + "/lib/zip_codes/pl/tasks/zip_codes_pl.rake"
59
+ ```
60
+
61
+ The output directory can also be set once:
62
+
63
+ ```ruby
64
+ ZipCodes::PL.configure do |config|
65
+ config.output_dir = Rails.root.join("db/pna").to_s
66
+ config.poczta_request_interval = 0.5 # optionally go even slower
67
+ end
68
+ ```
69
+
70
+ or through the `ZIP_CODES_PL_DIR` environment variable. Reads look there first
71
+ and fall back to the bundled dataset, so configuring a directory that has not
72
+ been refreshed yet changes nothing.
73
+
74
+ ## Usage
75
+
76
+ ```ruby
77
+ ZipCodes::PL.find_by_postal_code("86-010") # "86010" works too
78
+ # => [#<data ZipCodes::PL::Record postal_code="86-010", city="Koronowo", ...>, ...]
79
+
80
+ ZipCodes::PL.find_by_city("zlotow") # case and diacritics insensitive
81
+ ZipCodes::PL.find_by_city("Koronowo", voivodeship: "kujawsko-pomorskie")
82
+ ZipCodes::PL.search_by_city("wie") # name fragment, e.g. "Nowa Wieś"
83
+ ZipCodes::PL.search_by_city("OS") # exactly "Oś"
84
+
85
+ ZipCodes::PL.voivodeships
86
+ # => 16 records: TERYT code, Polish name, ASCII slug
87
+ ```
88
+
89
+ Fragment searches ignore case and Polish diacritics. Queries of three or more
90
+ characters behave like `%LIKE%`; a two-character query only matches a complete
91
+ name, so the shortest place, `Oś`, remains searchable. One-character queries
92
+ return an empty result without scanning the file.
93
+
94
+ One record is one postal code in one place. A town with several codes has
95
+ several records, and so does a code shared by several villages - `86-010` covers
96
+ 42 places around Koronowo.
97
+
98
+ When you want **places rather than codes**, there is a ready aggregation:
99
+
100
+ ```ruby
101
+ ZipCodes::PL.cities.first
102
+ # => #<data ZipCodes::PL::City
103
+ # name="Abisynia", voivodeship="pomorskie", voivodeship_teryt="22",
104
+ # commune="Karsin", commune_teryt="220603",
105
+ # latitude=53.9244, longitude=17.9337, postal_codes=["83-440"]>
106
+ ```
107
+
108
+ A place's coordinate is the mean of its rows, because the source gives one point
109
+ per postal code and Bydgoszcz has 679 of them.
110
+
111
+ Places are identified by name **and commune**, not by name alone. Poland has 119
112
+ villages called `Nowa Wieś`, 28 of them in a single voivodeship; collapsing them
113
+ by name puts the resulting point in a field up to 158 km from the farthest one.
114
+
115
+ ### Importing into a database
116
+
117
+ Raw records can be imported in batches without loading the entire file:
118
+
119
+ ```ruby
120
+ ZipCodes::PL.each_record.each_slice(1000) do |batch|
121
+ PostalCode.upsert_all(batch.map(&:to_h))
122
+ end
123
+ ```
124
+
125
+ Or import the aggregated places:
126
+
127
+ ```ruby
128
+ ZipCodes::PL.cities.each_slice(1000) do |batch|
129
+ City.upsert_all(
130
+ batch.map do |city|
131
+ { name: city.name, province: city.voivodeship,
132
+ latitude: city.latitude, longitude: city.longitude }
133
+ end,
134
+ unique_by: %i[name province]
135
+ )
136
+ end
137
+ ```
138
+
139
+ ## File format
140
+
141
+ `zip-codes-pl.tsv` - tab separated with a header row, next to
142
+ `zip-codes-pl.manifest.json` holding the source ETag, build time and row count.
143
+ It is left uncompressed on purpose: the file is committed, and a refresh should
144
+ produce a reviewable diff rather than a fresh opaque blob.
145
+
146
+ | column | example | note |
147
+ |---|---|---|
148
+ | `postal_code` | `86-010` | |
149
+ | `city` | `Nowa Wieś Wielka` | |
150
+ | `voivodeship` | `kujawsko-pomorskie` | from this gem, not from the source |
151
+ | `voivodeship_teryt` | `04` | official TERYT code |
152
+ | `county` / `county_teryt` | `powiat bydgoski` / `0403` | Poczta Polska name joined by TERYT code |
153
+ | `commune` / `commune_teryt` | `Koronowo` / `040304` | Poczta Polska name joined by TERYT code |
154
+ | `latitude` / `longitude` | `53.3123` / `17.9539` | WGS84 |
155
+ | `accuracy` | `6` | as reported by the source - see below |
156
+
157
+ The file is written to a temporary path and renamed. A GeoNames download,
158
+ Poczta Polska response, parsing or validation failure leaves the previous TSV
159
+ and manifest unchanged.
160
+
161
+ ## What this gem does not pretend to do
162
+
163
+ **These are postal-code centroids, not surveyed points.** GeoNames derives many
164
+ of them algorithmically from place names, and falls back to an average of
165
+ neighbouring postal codes where no match is found. They are good enough to sort
166
+ by distance or draw on a map; they are not property-grade coordinates. The
167
+ `accuracy` column is a source field, and for Poland it is currently `6` on all
168
+ 72,899 rows, so it carries no usable signal.
169
+
170
+ **This is a postal dataset, not the official place register.** It covers places
171
+ that have a postal code in the GeoNames extract, under GeoNames' spelling. The
172
+ official Polish register (TERYT SIMC) is larger and authoritative on names; the
173
+ gap between the two has not been measured here.
174
+
175
+ **Administrative names are normalized by TERYT code.** The gem carries its own
176
+ table for the 16 voivodeships and fetches current county and commune names from
177
+ Poczta Polska during a refresh. The one explicit exception is historical code
178
+ `320304` for the former Ostrowice commune, abolished on 1 January 2019, which is
179
+ still present in GeoNames. **The `*_teryt` columns remain the dependable
180
+ identifiers**, not the names.
181
+
182
+ There are no streets or building numbers here, and this is not an address
183
+ geocoder. For address-level lookups Poland has a free official service at
184
+ `services.gugik.gov.pl/uug`.
185
+
186
+ ## Development
187
+
188
+ ```bash
189
+ bin/setup # or: bundle install
190
+ bundle exec rake test
191
+ bundle exec rubocop
192
+ ```
193
+
194
+ The test suite never touches the network: the source archive is generated in
195
+ memory and the HTTP transport is exercised against stubbed responses.
196
+
197
+ ## Source and licence
198
+
199
+ Postal data and coordinates come from [GeoNames](https://www.geonames.org)
200
+ under **CC BY 4.0**. County and commune names are fetched from the public
201
+ [Poczta Polska search](https://www.poczta-polska.pl/znajdz-kod-pocztowy/).
202
+ The manifest carries a ready attribution string.
203
+
204
+ The code is MIT licensed.
data/README.pl.md ADDED
@@ -0,0 +1,179 @@
1
+ # zip-codes-pl
2
+
3
+ *(English documentation: [README.md](README.md))*
4
+
5
+ Polskie kody pocztowe, miejscowości i województwa **ze współrzędnymi**, budowane
6
+ i odświeżane jednym zadaniem rake.
7
+
8
+ **Zbiór jedzie razem z gemem, więc nie trzeba niczego budować przed pierwszym
9
+ użyciem.** Zadanie rake odświeża go do wskazanego katalogu, pytając serwer
10
+ nagłówkiem `If-None-Match`, więc powtórne uruchomienie nie pobiera ani bajta,
11
+ kiedy źródło się nie zmieniło. Twoja odświeżona kopia ma pierwszeństwo przed
12
+ tą z gema.
13
+
14
+ Zbiór to **72 899 wierszy**, **20 299 kodów pocztowych** i **52 325
15
+ miejscowości** w 16 województwach, w pliku TSV o rozmiarze 6,6 MB. Wyszukiwanie
16
+ skanuje plik bez budowania indeksu i bez trzymania całego zbioru w pamięci.
17
+
18
+ ## Instalacja
19
+
20
+ ```ruby
21
+ gem "zip-codes-pl"
22
+ ```
23
+
24
+ ## Odświeżanie zbioru
25
+
26
+ GeoNames publikuje codziennie. Żeby wziąć nowszy zrzut niż ten w twojej wersji:
27
+
28
+ ```bash
29
+ rake zip_codes:pl:update # do katalogu domyślnego (data/)
30
+ rake zip_codes:pl:update[db/pna] # do wskazanego katalogu
31
+ rake zip_codes:pl:info[db/pna] # co jest zbudowane i z kiedy
32
+ ```
33
+
34
+ Kiedy zrzut GeoNames się zmienił, build dociąga z publicznej wyszukiwarki
35
+ Poczty Polskiej nazwy powiatów i gmin po kodach TERYT.
36
+ Zapytania idą sekwencyjnie, domyślnie nie częściej niż co 0,25 s; odpowiedź
37
+ `429` wstrzymuje kolejne zapytanie zgodnie z `Retry-After`. Pełne wzbogacenie
38
+ zajmuje obecnie około pięciu minut. Przy odpowiedzi `304 Not Modified` Poczta
39
+ Polska nie jest odpytywana.
40
+
41
+ W aplikacji Rails zadanie podpina się samo przez railtie. Poza Railsami dodaj do
42
+ swojego `Rakefile`:
43
+
44
+ ```ruby
45
+ load Gem::Specification.find_by_name("zip-codes-pl").gem_dir + "/lib/zip_codes/pl/tasks/zip_codes_pl.rake"
46
+ ```
47
+
48
+ Katalog wyjściowy ustawia się też na stałe:
49
+
50
+ ```ruby
51
+ ZipCodes::PL.configure do |config|
52
+ config.output_dir = Rails.root.join("db/pna").to_s
53
+ config.poczta_request_interval = 0.5 # opcjonalnie jeszcze wolniej
54
+ end
55
+ ```
56
+
57
+ albo zmienną `ZIP_CODES_PL_DIR`. Odczyt najpierw zagląda tam, a w razie braku
58
+ pliku sięga po zbiór z gema - ustawienie katalogu, którego jeszcze nie
59
+ odświeżyłeś, niczego nie psuje.
60
+
61
+ ## Użycie
62
+
63
+ ```ruby
64
+ ZipCodes::PL.find_by_postal_code("86-010") # działa też "86010"
65
+ # => [#<data ZipCodes::PL::Record postal_code="86-010", city="Koronowo", ...>, ...]
66
+
67
+ ZipCodes::PL.find_by_city("zlotow") # bez ogonków i wielkości liter
68
+ ZipCodes::PL.find_by_city("Koronowo", voivodeship: "kujawsko-pomorskie")
69
+ ZipCodes::PL.search_by_city("wie") # fragment nazwy, np. "Nowa Wieś"
70
+ ZipCodes::PL.search_by_city("OS") # dokładnie "Oś"
71
+
72
+ ZipCodes::PL.voivodeships
73
+ # => 16 rekordów: kod TERYT, nazwa, slug bez polskich znaków
74
+ ```
75
+
76
+ Wyszukiwanie fragmentu ignoruje wielkość liter i polskie znaki. Od trzech
77
+ znaków działa jak `%LIKE%`; dwuznakowa fraza dopasowuje tylko całą nazwę, dzięki
78
+ czemu nadal można znaleźć najkrótszą miejscowość „Oś”. Jednoznakowe zapytania
79
+ zwracają pusty wynik bez skanowania pliku.
80
+
81
+ Jeden rekord to jeden kod pocztowy w jednej miejscowości. Miejscowość z kilkoma
82
+ kodami ma kilka rekordów, tak samo kod dzielony przez kilka wsi - `86-010` to
83
+ 42 miejscowości wokół Koronowa.
84
+
85
+ Kiedy potrzebujesz **miejscowości, a nie kodów**, jest gotowa agregacja:
86
+
87
+ ```ruby
88
+ ZipCodes::PL.cities.first
89
+ # => #<data ZipCodes::PL::City
90
+ # name="Abisynia", voivodeship="pomorskie", voivodeship_teryt="22",
91
+ # commune="Karsin", commune_teryt="220603",
92
+ # latitude=53.9244, longitude=17.9337, postal_codes=["83-440"]>
93
+ ```
94
+
95
+ Współrzędne miejscowości to średnia z jej wierszy, bo źródło daje punkt na kod
96
+ pocztowy, a Bydgoszcz ma ich 679.
97
+
98
+ Miejscowość jest identyfikowana nazwą **i gminą**, nie samą nazwą. W Polsce jest
99
+ 119 wsi „Nowa Wieś", z czego 28 w jednym województwie; sklejenie ich po nazwie
100
+ wystawia punkt w polu, nawet 158 km od najdalszej z nich.
101
+
102
+ ### Import do bazy
103
+
104
+ Surowe rekordy można importować partiami bez wczytywania całego pliku:
105
+
106
+ ```ruby
107
+ ZipCodes::PL.each_record.each_slice(1000) do |batch|
108
+ PostalCode.upsert_all(batch.map(&:to_h))
109
+ end
110
+ ```
111
+
112
+ Albo zapisać zagregowane miejscowości:
113
+
114
+ ```ruby
115
+ ZipCodes::PL.cities.each_slice(1000) do |batch|
116
+ City.upsert_all(
117
+ batch.map do |city|
118
+ { name: city.name, province: city.voivodeship,
119
+ latitude: city.latitude, longitude: city.longitude }
120
+ end,
121
+ unique_by: %i[name province]
122
+ )
123
+ end
124
+ ```
125
+
126
+ ## Format pliku
127
+
128
+ `zip-codes-pl.tsv` - TSV z nagłówkiem, obok `zip-codes-pl.manifest.json` z
129
+ ETagiem źródła, datą budowy i liczbą wierszy. Celowo nieskompresowany: plik jest
130
+ commitowany, więc odświeżenie ma dawać diff do przejrzenia, a nie nowy nieczytelny
131
+ blob. Kolumny:
132
+
133
+ | kolumna | przykład | uwagi |
134
+ |---|---|---|
135
+ | `postal_code` | `86-010` | |
136
+ | `city` | `Nowa Wieś Wielka` | |
137
+ | `voivodeship` | `kujawsko-pomorskie` | nazwa z tego gema, nie ze źródła |
138
+ | `voivodeship_teryt` | `04` | urzędowy kod TERYT |
139
+ | `county` / `county_teryt` | `powiat bydgoski` / `0403` | nazwa z Poczty Polskiej po kodzie TERYT |
140
+ | `commune` / `commune_teryt` | `Koronowo` / `040304` | nazwa z Poczty Polskiej po kodzie TERYT |
141
+ | `latitude` / `longitude` | `53.3123` / `17.9539` | WGS84 |
142
+ | `accuracy` | `6` | pole źródła - patrz niżej |
143
+
144
+ Plik jest zapisywany przez plik tymczasowy i `rename`. Błąd pobierania GeoNames,
145
+ odpowiedzi Poczty Polskiej, parsowania albo walidacji zostawia poprzedni TSV i
146
+ manifest bez zmian.
147
+
148
+ ## Czego ten gem nie udaje
149
+
150
+ **To są centroidy kodów pocztowych, a nie punkty z pomiaru.** GeoNames wylicza
151
+ wiele z nich algorytmicznie z nazw miejscowości, a tam gdzie nie znajdzie
152
+ dopasowania, bierze średnią sąsiednich kodów. Nadają się do sortowania po
153
+ odległości i do mapy; nie są współrzędnymi geodezyjnymi. Kolumna `accuracy`
154
+ pochodzi ze źródła i dla Polski ma obecnie wartość `6` we **wszystkich** 72 899
155
+ wierszach, więc nie niesie żadnej informacji.
156
+
157
+ **To jest zbiór pocztowy, nie urzędowy rejestr miejscowości.** Obejmuje
158
+ miejscowości, które mają kod pocztowy w zrzucie GeoNames, w pisowni GeoNames.
159
+ Urzędowy TERYT SIMC jest większy i to on rozstrzyga o nazwach; różnicy między
160
+ nimi tutaj nie zmierzyłem.
161
+
162
+ **Nazwy administracyjne są ujednolicone po kodach TERYT.** Gem ma własną tabelę
163
+ 16 województw, a podczas odświeżenia pobiera z Poczty Polskiej aktualne nazwy
164
+ powiatów i gmin. Jedynym jawnym wyjątkiem jest historyczny kod `320304` dawnej
165
+ gminy Ostrowice, zniesionej 1 stycznia 2019, który nadal występuje w GeoNames.
166
+ **Pewnym identyfikatorem pozostają kolumny `*_teryt`**, nie nazwy.
167
+
168
+ Gem nie zawiera ulic ani numerów budynków i nie jest geokoderem adresów. Do
169
+ adresu z dokładnością do numeru służy darmowe UUG GUGiK
170
+ (`services.gugik.gov.pl/uug`).
171
+
172
+ ## Źródło i licencja
173
+
174
+ Dane PNA i współrzędne pochodzą z [GeoNames](https://www.geonames.org) na
175
+ licencji **CC BY 4.0**. Nazwy powiatów i gmin są pobierane z publicznej
176
+ wyszukiwarki [Poczty Polskiej](https://www.poczta-polska.pl/znajdz-kod-pocztowy/).
177
+ Manifest niesie gotową formułkę atrybucji.
178
+
179
+ Sam kod jest na licencji MIT.
@@ -0,0 +1,10 @@
1
+ {
2
+ "source_url": "https://download.geonames.org/export/zip/PL.zip",
3
+ "attribution": "GeoNames (https://www.geonames.org), CC BY 4.0; county and commune names: Poczta Polska (https://www.poczta-polska.pl)",
4
+ "etag": "\"f1225-65b68b320a969\"",
5
+ "last_modified": "Mon, 14 Sep 2026 03:05:00 GMT",
6
+ "row_count": 72899,
7
+ "built_at": "2026-09-14T23:25:36Z",
8
+ "gem_version": "0.1.1",
9
+ "format_version": 1
10
+ }