seaports 1.0.0 → 1.2.0
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 +4 -4
- data/CHANGELOG.md +16 -0
- data/README.md +101 -6
- data/data/un_locode_seaports.csv +17528 -17521
- data/lib/seaports/boundaries.rb +176 -0
- data/lib/seaports/builder.rb +95 -4
- data/lib/seaports/diff.rb +75 -5
- data/lib/seaports/subdivisions.rb +80 -0
- data/lib/seaports/version.rb +1 -1
- data/lib/seaports.rb +49 -2
- metadata +7 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 878655485e54ba5bb3f0ac454ff4ca7279d4725c8672325221cca9955b314d7c
|
|
4
|
+
data.tar.gz: c453a0e9b208fc027ea45d722ba13e7472a14785dfc534b4b936e367f204953a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 16c90431ed6304d4404ff2f02f816abf44bc1ed93fbb7bfbe453ab8303c662616ca27b4d8daf9a725d8edc27afa7622fdf596b99b51a0dc4fd75cd43416ab0c3
|
|
7
|
+
data.tar.gz: c9923419e7a8567153cd0c2e2643496cc3335089ca05dbcc2a3377132f8f7e96b7ed30f63cf66e1322dc927b189a6fbfa45a6066f8e5dd622ee65a84a1716630
|
data/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,22 @@
|
|
|
3
3
|
Every entry below the first is a UN/LOCODE refresh, opened as a pull request by
|
|
4
4
|
`.github/workflows/refresh.yml` and merged by a person.
|
|
5
5
|
|
|
6
|
+
## 1.2.0 — 2026-10-20
|
|
7
|
+
|
|
8
|
+
Each port now carries its ISO 3166-2 subdivision, the path of every subdivision above it, and the principal (top-level) one: `Seaports.subdivision("GBFXT")` is `"GB-SFK"` (Suffolk), `Seaports.subdivision_path("FRSXB")` is `["FR-67", "FR-6AE", "FR-GES"]`, `Seaports.principal_subdivision("GBFXT")` is `"GB-ENG"`. Match on whichever level of the path other data uses: the principal one usually, but not for Alsace-Moselle, Irish counties or Indonesian provinces.
|
|
9
|
+
|
|
10
|
+
14,567 of 17,527 ports have one: 13,149 from UN/LOCODE's subdivision column, 1,312 placed by their coordinates on Natural Earth's states-and-provinces map (Amsterdam), and 106 from a reviewed list for ports neither places (Valencia, Algeciras, Barcelona). Every code is checked against ISO 3166-2 (Debian iso-codes) at build time.
|
|
11
|
+
|
|
12
|
+
`Seaports::Port` gains three members, all optional, so `Port.new(locode:, name:, coordinates:)` still works. A positional pattern match on a three-element Port (`in [locode, name, coordinates]`) no longer matches; match by key instead.
|
|
13
|
+
|
|
14
|
+
Refreshes now ship a change at any level of a port's path, refuse a rebuild that moves more than 2% of ports to another principal subdivision, report overrides that have outlived their reason, and turn a failed download into a refusal issue rather than a failed build.
|
|
15
|
+
|
|
16
|
+
Same UN/LOCODE 2024-2 ports, names and positions as 1.1.0.
|
|
17
|
+
|
|
18
|
+
## 1.1.0 — 2026-10-19
|
|
19
|
+
|
|
20
|
+
UN/LOCODE release unknown. 17527 sea ports (17520 before): 7 added, 0 removed, 1 renamed, 83 repositioned.
|
|
21
|
+
|
|
6
22
|
## 1.0.0 — 2026-08-21
|
|
7
23
|
|
|
8
24
|
First release. Extracted from the Trackberry application, where this table
|
data/README.md
CHANGED
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
A locode in, a port name and a position out.
|
|
7
7
|
|
|
8
8
|
```ruby
|
|
9
|
-
Seaports.name("CNXMG")
|
|
10
|
-
Seaports.coordinates("AUBNE")
|
|
11
|
-
Seaports.
|
|
9
|
+
Seaports.name("CNXMG") # => "Xiamen Pt"
|
|
10
|
+
Seaports.coordinates("AUBNE") # => { "lat" => -27.4667, "lng" => 153.0167 }
|
|
11
|
+
Seaports.principal_subdivision("DEHAM") # => "DE-HH"
|
|
12
|
+
Seaports.find("KRPUS") # => #<data Seaports::Port locode="KRPUS", name="Busan", ...>
|
|
12
13
|
```
|
|
13
14
|
|
|
14
15
|
Tracking feeds speak in codes. An AIS provider names some port calls and sends
|
|
@@ -29,11 +30,12 @@ out of the default gems.
|
|
|
29
30
|
|
|
30
31
|
## What is in it
|
|
31
32
|
|
|
32
|
-
Every entry UN/LOCODE classifies as a sea port: **17,
|
|
33
|
-
with published coordinates, built from
|
|
33
|
+
Every entry UN/LOCODE classifies as a sea port: **17,527 ports**, 11,820 of them
|
|
34
|
+
with published coordinates and 14,567 with a subdivision, built from
|
|
35
|
+
**UN/LOCODE 2024-2**.
|
|
34
36
|
|
|
35
37
|
```ruby
|
|
36
|
-
Seaports.count # =>
|
|
38
|
+
Seaports.count # => 17527
|
|
37
39
|
Seaports.all # => [#<data Seaports::Port ...>, ...]
|
|
38
40
|
Seaports.data_release # => "2024-2"
|
|
39
41
|
```
|
|
@@ -53,6 +55,63 @@ Roughly a third of the table has no position published. That is a fact about
|
|
|
53
55
|
the source, not a failure — the name is still worth having, and a map simply
|
|
54
56
|
draws no marker.
|
|
55
57
|
|
|
58
|
+
### Subdivisions
|
|
59
|
+
|
|
60
|
+
UN/LOCODE files each place under a subdivision of its country, which joined to
|
|
61
|
+
the country is an ISO 3166-2 code. It picks whatever level suits the country:
|
|
62
|
+
a state for Hamburg, a county for Felixstowe. Each port carries that code, the
|
|
63
|
+
**path** of every ISO subdivision above it, and the **principal subdivision**
|
|
64
|
+
at the top:
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
Seaports.subdivision("GBFXT") # => "GB-SFK" Suffolk
|
|
68
|
+
Seaports.principal_subdivision("GBFXT") # => "GB-ENG" England
|
|
69
|
+
Seaports.subdivision_path("FRSXB") # => ["FR-67", "FR-6AE", "FR-GES"] Bas-Rhin › Alsace › Grand Est
|
|
70
|
+
Seaports.principal_subdivision("DEHAM") # => "DE-HH" Hamburg, already top level
|
|
71
|
+
Seaports.principal_subdivision("ESVLC") # => "ES-VC" Comunitat Valenciana
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
To join a port to other data, a public holiday calendar say, match on
|
|
75
|
+
whichever level of the path that data uses. The principal subdivision is
|
|
76
|
+
usually the one (German states, UK nations, Spanish communities), but ISO's
|
|
77
|
+
top level is not always where things are decided: Alsace-Moselle (`FR-6AE`)
|
|
78
|
+
keeps holidays the rest of Grand Est does not, Ireland's top level is its four
|
|
79
|
+
provinces rather than its counties, and Indonesia's is island groups (`ID-JW`
|
|
80
|
+
Java) rather than provinces (`ID-JK` Jakarta). The path carries every level, so
|
|
81
|
+
matching on any of it is always safe.
|
|
82
|
+
|
|
83
|
+
A port's subdivision comes from the first of three sources that has one, and
|
|
84
|
+
the table records which (`subdivision_source`):
|
|
85
|
+
|
|
86
|
+
| Source | Ports | How |
|
|
87
|
+
| --- | --- | --- |
|
|
88
|
+
| `override` | 106 | A reviewed list, `data/sources/subdivision_overrides.csv`, for ports neither of the others places: Valencia, Algeciras and Barcelona have no subdivision and no position in UN/LOCODE. Each row says where its code came from. |
|
|
89
|
+
| `unlocode` | 13,149 | UN/LOCODE's own subdivision column. |
|
|
90
|
+
| `boundaries` | 1,312 | The port's coordinates placed on [Natural Earth](https://www.naturalearthdata.com/)'s states-and-provinces map (public domain, pinned to one commit), or the nearest subdivision within 10 km when a waterfront position lands just offshore. Amsterdam is placed this way. |
|
|
91
|
+
|
|
92
|
+
Every code is checked against ISO 3166-2, and the path follows ISO's own
|
|
93
|
+
hierarchy, taken from
|
|
94
|
+
[Debian's iso-codes](https://salsa.debian.org/iso-codes-team/iso-codes). A code
|
|
95
|
+
whose chain is broken in that list is dropped rather than shipped without a
|
|
96
|
+
trustworthy top. Both maps are read when the table is built; nothing is
|
|
97
|
+
fetched at runtime.
|
|
98
|
+
|
|
99
|
+
2,960 ports stay without one, mostly offshore fields and moorings, and ports in
|
|
100
|
+
countries with no regional distinctions worth drawing. To place more, run
|
|
101
|
+
`ruby script/suggest_subdivision_overrides.rb candidates.csv`: it searches each
|
|
102
|
+
port's name on OpenStreetMap's Nominatim and prints suggestions with the place
|
|
103
|
+
it matched, for a person to check before adding them to the overrides file. A
|
|
104
|
+
name search does find the wrong Puerto de Santa María. Place names in the
|
|
105
|
+
overrides file are © OpenStreetMap contributors, under the
|
|
106
|
+
[ODbL](https://www.openstreetmap.org/copyright).
|
|
107
|
+
|
|
108
|
+
Each refresh lists, for review, overrides that have outlived their reason: a
|
|
109
|
+
port that left UN/LOCODE, or one UN/LOCODE now places itself.
|
|
110
|
+
|
|
111
|
+
`Seaports::Port` gained `subdivision`, `principal_subdivision` and
|
|
112
|
+
`subdivision_path` in 1.2.0, all optional, so a `Port` built from locode, name
|
|
113
|
+
and coordinates as before still works.
|
|
114
|
+
|
|
56
115
|
### What is deliberately not in it
|
|
57
116
|
|
|
58
117
|
Only entries UNECE marks with function `1`, "port", are kept. A road terminal,
|
|
@@ -73,6 +132,42 @@ from a three-character carrier code, which is a heuristic: a code that is not a
|
|
|
73
132
|
locode tail could pair with a country to form a valid-looking locode for some
|
|
74
133
|
inland village, and a table of ports is far less likely to name it.
|
|
75
134
|
|
|
135
|
+
## What it costs
|
|
136
|
+
|
|
137
|
+
A lookup is a hash fetch. The table is parsed once, on the first lookup rather
|
|
138
|
+
than at require time, and after that nothing touches the disk again.
|
|
139
|
+
|
|
140
|
+
| | |
|
|
141
|
+
| --- | --- |
|
|
142
|
+
| lookup | **0.31 µs** — about 3.2M per second |
|
|
143
|
+
| miss | 0.28 µs |
|
|
144
|
+
| malformed input | 0.25 µs — rejected on shape before the table is consulted |
|
|
145
|
+
| first lookup, which parses the table | ~140 ms, once per process |
|
|
146
|
+
| resident table | 3.9 MB |
|
|
147
|
+
|
|
148
|
+
Measured on Ruby 4.0, Apple Silicon, against the shipped 468 KB table. Reading
|
|
149
|
+
the file off disk is 0.4 ms of that first 140 ms; the rest is CSV parsing and
|
|
150
|
+
building the 17,520 objects, so it is CPU rather than I/O.
|
|
151
|
+
|
|
152
|
+
There is no database, no ActiveRecord, and no query per lookup. That is worth
|
|
153
|
+
saying because the alternative approach in this corner of the ecosystem is to
|
|
154
|
+
ship a SQLite file and read it through ActiveRecord, which puts a second
|
|
155
|
+
database connection and a version-pinned ORM into the dependency graph of an
|
|
156
|
+
application that only wanted to turn `CNXMG` into `Xiamen Pt`. A CSV and a hash
|
|
157
|
+
answer the same question with one dependency and no connection.
|
|
158
|
+
|
|
159
|
+
Nothing here is cached, because there is nothing left to cache: the table
|
|
160
|
+
already is the cache. A `Rails.cache` round trip costs around a millisecond,
|
|
161
|
+
which is some three thousand times slower than the lookup it would be caching.
|
|
162
|
+
|
|
163
|
+
The one cost worth managing is that first 140 ms, and the fix is to move it off
|
|
164
|
+
the request path rather than to cache it:
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
# config/initializers/seaports.rb
|
|
168
|
+
Rails.application.config.after_initialize { Seaports.count }
|
|
169
|
+
```
|
|
170
|
+
|
|
76
171
|
## How the table stays current
|
|
77
172
|
|
|
78
173
|
UNECE republishes twice a year and does not announce it. A scheduled workflow
|