addressing 2.1.0 → 2.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 +32 -0
- data/README.md +223 -112
- data/data/UPSTREAM_VERSION +1 -0
- data/data/address_formats.json +2256 -206
- data/data/countries.json +1282 -0
- data/data/locale.json +222 -0
- data/data/subdivision/BR-AC.json +22 -22
- data/data/subdivision/BR-AL.json +102 -102
- data/data/subdivision/BR-AM.json +62 -62
- data/data/subdivision/BR-AP.json +16 -16
- data/data/subdivision/BR-BA.json +417 -417
- data/data/subdivision/BR-CE.json +184 -184
- data/data/subdivision/BR-DF.json +1 -1
- data/data/subdivision/BR-ES.json +79 -79
- data/data/subdivision/BR-GO.json +246 -246
- data/data/subdivision/BR-MA.json +217 -217
- data/data/subdivision/BR-MG.json +853 -853
- data/data/subdivision/BR-MS.json +78 -78
- data/data/subdivision/BR-MT.json +141 -141
- data/data/subdivision/BR-PA.json +144 -144
- data/data/subdivision/BR-PB.json +223 -223
- data/data/subdivision/BR-PE.json +185 -185
- data/data/subdivision/BR-PI.json +223 -223
- data/data/subdivision/BR-PR.json +400 -400
- data/data/subdivision/BR-RJ.json +93 -93
- data/data/subdivision/BR-RN.json +166 -166
- data/data/subdivision/BR-RO.json +52 -52
- data/data/subdivision/BR-RR.json +15 -15
- data/data/subdivision/BR-RS.json +497 -497
- data/data/subdivision/BR-SC.json +295 -295
- data/data/subdivision/BR-SE.json +75 -75
- data/data/subdivision/BR-SP.json +645 -645
- data/data/subdivision/BR-TO.json +139 -139
- data/data/subdivision/CL-AI.json +10 -10
- data/data/subdivision/CL-AN.json +9 -9
- data/data/subdivision/CL-AP.json +4 -4
- data/data/subdivision/CL-AR.json +32 -32
- data/data/subdivision/CL-AT.json +9 -9
- data/data/subdivision/CL-BI.json +34 -34
- data/data/subdivision/CL-CO.json +15 -15
- data/data/subdivision/CL-LI.json +33 -33
- data/data/subdivision/CL-LL.json +30 -30
- data/data/subdivision/CL-LR.json +12 -12
- data/data/subdivision/CL-MA.json +11 -11
- data/data/subdivision/CL-ML.json +30 -30
- data/data/subdivision/CL-NB.json +21 -21
- data/data/subdivision/CL-RM.json +52 -52
- data/data/subdivision/CL-TA.json +7 -7
- data/data/subdivision/CL-VS.json +38 -38
- data/data/subdivision/CV.json +9 -9
- data/data/subdivision/KY.json +3 -3
- data/data/subdivision/TV.json +1 -1
- data/lib/addressing/address.rb +7 -3
- data/lib/addressing/address_format.rb +55 -81
- data/lib/addressing/address_validator.rb +98 -0
- data/lib/addressing/blank.rb +32 -0
- data/lib/addressing/country.rb +28 -314
- data/lib/addressing/data_source.rb +75 -0
- data/lib/addressing/default_formatter.rb +25 -25
- data/lib/addressing/enum.rb +2 -6
- data/lib/addressing/field_violation.rb +17 -0
- data/lib/addressing/lazy_subdivisions.rb +16 -15
- data/lib/addressing/locale.rb +20 -269
- data/lib/addressing/model.rb +20 -78
- data/lib/addressing/postal_label_formatter.rb +16 -15
- data/lib/addressing/subdivision.rb +113 -116
- data/lib/addressing/version.rb +1 -1
- data/lib/addressing.rb +6 -1
- metadata +9 -3
- data/data/address_formats.dump +0 -0
|
@@ -16,7 +16,53 @@ module Addressing
|
|
|
16
16
|
# @example Get subdivisions for a Brazilian state
|
|
17
17
|
# municipalities = Addressing::Subdivision.all(['BR', 'CE'])
|
|
18
18
|
class Subdivision
|
|
19
|
+
# The subdivision chain of an address.
|
|
20
|
+
#
|
|
21
|
+
# subdivisions holds the matched predefined subdivisions, ordered from the
|
|
22
|
+
# administrative area downward. unmatched_level is the position, in the
|
|
23
|
+
# values given to Subdivision.chain, of the value that matched no
|
|
24
|
+
# predefined subdivision, or nil when there was none.
|
|
25
|
+
Chain = Data.define(:subdivisions, :unmatched_level)
|
|
26
|
+
|
|
19
27
|
class << self
|
|
28
|
+
# Resolves the subdivision chain for the values of the subdivision levels.
|
|
29
|
+
#
|
|
30
|
+
# The walk goes down the levels and stops at the first empty value, at
|
|
31
|
+
# the first value that matches no predefined subdivision, or at a
|
|
32
|
+
# subdivision without children.
|
|
33
|
+
#
|
|
34
|
+
# @param country_code [String] Country code
|
|
35
|
+
# @param values [Array<String, nil>] Values in level order (administrative area, locality, dependent locality)
|
|
36
|
+
# @return [Chain]
|
|
37
|
+
#
|
|
38
|
+
# @example
|
|
39
|
+
# chain = Addressing::Subdivision.chain("BR", ["CE", "Fortaleza"])
|
|
40
|
+
# chain.subdivisions.map(&:code) # => ["CE", "Fortaleza"]
|
|
41
|
+
# chain.unmatched_level # => nil
|
|
42
|
+
def chain(country_code, values)
|
|
43
|
+
parents = [country_code.upcase]
|
|
44
|
+
subdivisions = []
|
|
45
|
+
|
|
46
|
+
# Nothing to match against.
|
|
47
|
+
return Chain.new(subdivisions: subdivisions, unmatched_level: nil) if load_definitions(parents).empty?
|
|
48
|
+
|
|
49
|
+
values.each_with_index do |value, level|
|
|
50
|
+
# This level is empty, so there can be no sublevels.
|
|
51
|
+
break if Blank.blank?(value)
|
|
52
|
+
|
|
53
|
+
subdivision = get(value, parents)
|
|
54
|
+
return Chain.new(subdivisions: subdivisions, unmatched_level: level) if subdivision.nil?
|
|
55
|
+
|
|
56
|
+
subdivisions << subdivision
|
|
57
|
+
# No predefined subdivisions below this level, stop here.
|
|
58
|
+
break unless subdivision.children?
|
|
59
|
+
|
|
60
|
+
parents += [value]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
Chain.new(subdivisions: subdivisions, unmatched_level: nil)
|
|
64
|
+
end
|
|
65
|
+
|
|
20
66
|
# Gets a Subdivision instance by ID and parent hierarchy.
|
|
21
67
|
#
|
|
22
68
|
# @param id [String] Subdivision ID
|
|
@@ -24,7 +70,10 @@ module Addressing
|
|
|
24
70
|
# @return [Subdivision, nil] Subdivision instance or nil if not found
|
|
25
71
|
def get(id, parents)
|
|
26
72
|
definitions = load_definitions(parents)
|
|
27
|
-
|
|
73
|
+
# No matching definition found.
|
|
74
|
+
return nil unless definitions.dig("subdivisions", id)
|
|
75
|
+
|
|
76
|
+
create_subdivision_from_definitions(id, definitions, load_parent(definitions))
|
|
28
77
|
end
|
|
29
78
|
|
|
30
79
|
# Returns all subdivision instances for the provided parents.
|
|
@@ -35,9 +84,18 @@ module Addressing
|
|
|
35
84
|
definitions = load_definitions(parents)
|
|
36
85
|
return {} if definitions.empty?
|
|
37
86
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
87
|
+
# The siblings share one parent, so it is loaded once.
|
|
88
|
+
parent = load_parent(definitions)
|
|
89
|
+
definitions["subdivisions"].keys.to_h { |id| [id, create_subdivision_from_definitions(id, definitions, parent)] }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Checks whether there are subdivisions for the provided parents, without building them.
|
|
93
|
+
#
|
|
94
|
+
# @api private
|
|
95
|
+
# @param parents [Array<String>] Parent hierarchy (e.g., ['BR'] or ['BR', 'CE'])
|
|
96
|
+
# @return [Boolean]
|
|
97
|
+
def any?(parents)
|
|
98
|
+
!load_definitions(parents).fetch("subdivisions", {}).empty?
|
|
41
99
|
end
|
|
42
100
|
|
|
43
101
|
# Returns a list of subdivisions for the provided parents.
|
|
@@ -47,83 +105,65 @@ module Addressing
|
|
|
47
105
|
|
|
48
106
|
use_local_name = Locale.match_candidates(locale, definitions["locale"] || "")
|
|
49
107
|
|
|
50
|
-
definitions["subdivisions"].
|
|
51
|
-
subdivisions[id] = use_local_name ? definition["local_name"] : definition["name"]
|
|
52
|
-
end
|
|
108
|
+
definitions["subdivisions"].transform_values { |definition| use_local_name ? definition["local_name"] : definition["name"] }
|
|
53
109
|
end
|
|
54
110
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
#
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
# After the first level it is possible for predefined subdivisions
|
|
66
|
-
# to exist at a given level, but not for that specific parent.
|
|
67
|
-
# That's why the parent definition has the most precise answer.
|
|
68
|
-
grandparents = parents.dup
|
|
69
|
-
parent_id = grandparents.pop
|
|
70
|
-
parent_group = build_group(grandparents.dup)
|
|
71
|
-
@definitions ||= {}
|
|
72
|
-
|
|
73
|
-
if @definitions.dig(parent_group, "subdivisions", parent_id)
|
|
74
|
-
definition = @definitions[parent_group]["subdivisions"][parent_id]
|
|
75
|
-
return !!definition["has_children"]
|
|
76
|
-
else
|
|
77
|
-
# The parent definition wasn't loaded previously, fallback to
|
|
78
|
-
# guessing based on the count of subdivision data fields.
|
|
79
|
-
return parents.size <= subdivision_fields.size
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
# The first level has always data.
|
|
84
|
-
true
|
|
85
|
-
end
|
|
111
|
+
# Gets the key of the subdivision group for the provided parents.
|
|
112
|
+
#
|
|
113
|
+
# The key names the data file of the subdivision group. The data sync
|
|
114
|
+
# names the files with it, so this is the only home of the naming rule.
|
|
115
|
+
#
|
|
116
|
+
# @api private
|
|
117
|
+
# @param parents [Array<String>] Parent hierarchy (e.g., ['BR'] or ['BR', 'CE'])
|
|
118
|
+
# @return [String]
|
|
119
|
+
def group_key(parents)
|
|
120
|
+
raise ArgumentError, "The parents argument must not be empty." if parents.empty?
|
|
86
121
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
group = build_group(parents.dup)
|
|
91
|
-
if @definitions.key?(group)
|
|
92
|
-
return @definitions[group]
|
|
93
|
-
end
|
|
122
|
+
# Country codes are matched case-insensitively, subdivision IDs are not.
|
|
123
|
+
country_code = parents[0].upcase
|
|
124
|
+
subdivision_ids = parents.drop(1)
|
|
94
125
|
|
|
95
|
-
|
|
126
|
+
return country_code if subdivision_ids.empty?
|
|
96
127
|
|
|
97
|
-
#
|
|
98
|
-
if
|
|
99
|
-
filename = File.join(File.expand_path("../../../data/subdivision", __FILE__).to_s, "#{group}.json")
|
|
128
|
+
# The second parent is an ISO code, it can be used as-is.
|
|
129
|
+
return "#{country_code}-#{subdivision_ids[0]}" if subdivision_ids.length == 1 && subdivision_ids[0].length <= 3
|
|
100
130
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
131
|
+
# A dash per key allows the depth to be guessed later.
|
|
132
|
+
# Hash the remaining keys to ensure that the group is ASCII safe.
|
|
133
|
+
country_code + "-" * subdivision_ids.length + Digest::SHA1.hexdigest(subdivision_ids.join("-"))
|
|
134
|
+
end
|
|
105
135
|
|
|
106
|
-
|
|
136
|
+
# Gets the parents of a subdivision group from its definitions.
|
|
137
|
+
#
|
|
138
|
+
# @api private
|
|
139
|
+
# @param definitions [Hash] Definitions of a subdivision group
|
|
140
|
+
# @return [Array<String>]
|
|
141
|
+
def parents_of(definitions)
|
|
142
|
+
# The 'parents' key is omitted when it contains just the country code.
|
|
143
|
+
definitions["parents"] || [definitions["country_code"]]
|
|
107
144
|
end
|
|
108
145
|
|
|
109
|
-
|
|
146
|
+
protected
|
|
147
|
+
|
|
148
|
+
# Loads the subdivision definitions for the provided parents.
|
|
110
149
|
#
|
|
111
|
-
#
|
|
112
|
-
def
|
|
113
|
-
definitions
|
|
114
|
-
definitions.is_a?(Hash) ? definitions : {}
|
|
115
|
-
rescue JSON::ParserError
|
|
116
|
-
{}
|
|
150
|
+
# A subdivision group without a data file has no definitions.
|
|
151
|
+
def load_definitions(parents)
|
|
152
|
+
Addressing.data_source.fetch("subdivision/#{group_key(parents)}") { |definitions| process_definitions(definitions) } || {}
|
|
117
153
|
end
|
|
118
154
|
|
|
119
155
|
# Processes the loaded definitions.
|
|
120
156
|
#
|
|
121
157
|
# Adds keys and values that were removed from the JSON files for brevity.
|
|
122
158
|
def process_definitions(definitions)
|
|
123
|
-
#
|
|
124
|
-
|
|
159
|
+
# Definitions of the wrong shape are treated as if they didn't exist.
|
|
160
|
+
# The data verifier reports them, malformed JSON raises in the data source.
|
|
161
|
+
return {} unless definitions.is_a?(Hash) && definitions["subdivisions"].is_a?(Hash)
|
|
125
162
|
|
|
126
163
|
definitions["subdivisions"].each do |id, definition|
|
|
164
|
+
# Upstream writes a definition without keys as an empty JSON array.
|
|
165
|
+
definition = definitions["subdivisions"][id] = {} unless definition.is_a?(Hash)
|
|
166
|
+
|
|
127
167
|
# Add common keys from the root level.
|
|
128
168
|
definition["country_code"] = definitions["country_code"]
|
|
129
169
|
definition["id"] = id
|
|
@@ -144,7 +184,7 @@ module Addressing
|
|
|
144
184
|
|
|
145
185
|
# The code and local_code values are only specified if they
|
|
146
186
|
# don't match the name and local_name ones.
|
|
147
|
-
if !definition.key?("code")
|
|
187
|
+
if !definition.key?("code")
|
|
148
188
|
definition["code"] = definition["name"]
|
|
149
189
|
end
|
|
150
190
|
|
|
@@ -156,66 +196,23 @@ module Addressing
|
|
|
156
196
|
definitions
|
|
157
197
|
end
|
|
158
198
|
|
|
159
|
-
#
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
raise ArgumentError, "The parents argument must not be empty." if parents.empty?
|
|
164
|
-
|
|
165
|
-
return parents[0] if parents.length == 1
|
|
166
|
-
|
|
167
|
-
# The second parent is an ISO code, it can be used as-is.
|
|
168
|
-
return parents.join("-") if parents.length == 2 && parents[1].length <= 3
|
|
169
|
-
|
|
170
|
-
country_code = parents.shift
|
|
171
|
-
group = country_code
|
|
172
|
-
|
|
173
|
-
# A dash per key allows the depth to be guessed later.
|
|
174
|
-
group += "-" * parents.length
|
|
175
|
-
# Hash the remaining keys to ensure that the group is ASCII safe.
|
|
176
|
-
group + Digest::SHA1.hexdigest(parents.join("-"))
|
|
199
|
+
# Loads the parent of a subdivision group, if known.
|
|
200
|
+
def load_parent(definitions)
|
|
201
|
+
parents = parents_of(definitions)
|
|
202
|
+
get(parents[-1], parents[0...-1]) if parents.size > 1
|
|
177
203
|
end
|
|
178
204
|
|
|
179
205
|
# Creates a subdivision object from the provided definitions.
|
|
180
|
-
def create_subdivision_from_definitions(id, definitions)
|
|
181
|
-
if !definitions.dig("subdivisions", id)
|
|
182
|
-
# No matching definition found.
|
|
183
|
-
return nil
|
|
184
|
-
end
|
|
185
|
-
|
|
206
|
+
def create_subdivision_from_definitions(id, definitions, parent)
|
|
186
207
|
definition = definitions["subdivisions"][id]
|
|
187
|
-
|
|
188
|
-
definitions["parents"] = [definitions["country_code"]] unless definitions.key?("parents")
|
|
189
|
-
parents = definitions["parents"]
|
|
190
|
-
|
|
191
|
-
definition["parent"] = nil
|
|
192
|
-
|
|
193
|
-
# Load the parent, if known.
|
|
194
|
-
if parents.size > 1
|
|
195
|
-
grandparents = parents.dup
|
|
196
|
-
parent_id = grandparents.pop
|
|
197
|
-
parent_group = build_group(grandparents.dup)
|
|
198
|
-
@parents ||= {}
|
|
199
|
-
|
|
200
|
-
if !@parents.dig(parent_group, parent_id)
|
|
201
|
-
@parents[parent_group] ||= {}
|
|
202
|
-
@parents[parent_group][parent_id] = get(parent_id, grandparents)
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
definition["parent"] = @parents[parent_group][parent_id]
|
|
206
|
-
end
|
|
208
|
+
parents = parents_of(definitions)
|
|
207
209
|
|
|
208
210
|
# Prepare children.
|
|
209
|
-
|
|
210
|
-
children_parents = parents.dup
|
|
211
|
-
children_parents << id
|
|
212
|
-
|
|
213
|
-
definition["children"] = LazySubdivisions.new(children_parents)
|
|
214
|
-
end
|
|
211
|
+
children = definition["has_children"] ? LazySubdivisions.new(parents + [id]) : {}
|
|
215
212
|
|
|
216
213
|
new(
|
|
217
214
|
id: id,
|
|
218
|
-
parent:
|
|
215
|
+
parent: parent,
|
|
219
216
|
country_code: definition["country_code"],
|
|
220
217
|
locale: definition["locale"],
|
|
221
218
|
code: definition["code"],
|
|
@@ -223,7 +220,7 @@ module Addressing
|
|
|
223
220
|
name: definition["name"],
|
|
224
221
|
local_name: definition["local_name"],
|
|
225
222
|
postal_code_pattern: definition["postal_code_pattern"],
|
|
226
|
-
children:
|
|
223
|
+
children: children
|
|
227
224
|
)
|
|
228
225
|
end
|
|
229
226
|
end
|
data/lib/addressing/version.rb
CHANGED
data/lib/addressing.rb
CHANGED
|
@@ -3,22 +3,28 @@
|
|
|
3
3
|
# stdlib
|
|
4
4
|
require "cgi"
|
|
5
5
|
require "digest"
|
|
6
|
+
require "forwardable"
|
|
6
7
|
require "json"
|
|
7
8
|
|
|
8
9
|
# modules
|
|
9
10
|
require "addressing/exceptions"
|
|
10
11
|
require "addressing/enum"
|
|
12
|
+
require "addressing/blank"
|
|
11
13
|
require "addressing/address"
|
|
12
14
|
require "addressing/address_field"
|
|
13
15
|
require "addressing/address_format"
|
|
16
|
+
require "addressing/address_validator"
|
|
14
17
|
require "addressing/administrative_area_type"
|
|
15
18
|
require "addressing/country"
|
|
19
|
+
require "addressing/data_source"
|
|
16
20
|
require "addressing/default_formatter"
|
|
17
21
|
require "addressing/dependent_locality_type"
|
|
18
22
|
require "addressing/field_override"
|
|
23
|
+
require "addressing/field_violation"
|
|
19
24
|
require "addressing/lazy_subdivisions"
|
|
20
25
|
require "addressing/locale"
|
|
21
26
|
require "addressing/locality_type"
|
|
27
|
+
require "addressing/model"
|
|
22
28
|
require "addressing/postal_code_type"
|
|
23
29
|
require "addressing/postal_label_formatter"
|
|
24
30
|
require "addressing/subdivision"
|
|
@@ -26,7 +32,6 @@ require "addressing/version"
|
|
|
26
32
|
|
|
27
33
|
if defined?(ActiveSupport.on_load)
|
|
28
34
|
ActiveSupport.on_load(:active_record) do
|
|
29
|
-
require "addressing/model"
|
|
30
35
|
extend Addressing::Model
|
|
31
36
|
end
|
|
32
37
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: addressing
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Robin van der Vleuten
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-10-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description:
|
|
14
14
|
email: robinvdvleuten@gmail.com
|
|
@@ -19,8 +19,9 @@ files:
|
|
|
19
19
|
- CHANGELOG.md
|
|
20
20
|
- LICENSE
|
|
21
21
|
- README.md
|
|
22
|
-
- data/
|
|
22
|
+
- data/UPSTREAM_VERSION
|
|
23
23
|
- data/address_formats.json
|
|
24
|
+
- data/countries.json
|
|
24
25
|
- data/country/af.json
|
|
25
26
|
- data/country/ak.json
|
|
26
27
|
- data/country/am.json
|
|
@@ -169,6 +170,7 @@ files:
|
|
|
169
170
|
- data/country/zh-Hant.json
|
|
170
171
|
- data/country/zh.json
|
|
171
172
|
- data/country/zu.json
|
|
173
|
+
- data/locale.json
|
|
172
174
|
- data/subdivision/AD.json
|
|
173
175
|
- data/subdivision/AE.json
|
|
174
176
|
- data/subdivision/AM.json
|
|
@@ -719,13 +721,17 @@ files:
|
|
|
719
721
|
- lib/addressing/address.rb
|
|
720
722
|
- lib/addressing/address_field.rb
|
|
721
723
|
- lib/addressing/address_format.rb
|
|
724
|
+
- lib/addressing/address_validator.rb
|
|
722
725
|
- lib/addressing/administrative_area_type.rb
|
|
726
|
+
- lib/addressing/blank.rb
|
|
723
727
|
- lib/addressing/country.rb
|
|
728
|
+
- lib/addressing/data_source.rb
|
|
724
729
|
- lib/addressing/default_formatter.rb
|
|
725
730
|
- lib/addressing/dependent_locality_type.rb
|
|
726
731
|
- lib/addressing/enum.rb
|
|
727
732
|
- lib/addressing/exceptions.rb
|
|
728
733
|
- lib/addressing/field_override.rb
|
|
734
|
+
- lib/addressing/field_violation.rb
|
|
729
735
|
- lib/addressing/lazy_subdivisions.rb
|
|
730
736
|
- lib/addressing/locale.rb
|
|
731
737
|
- lib/addressing/locality_type.rb
|
data/data/address_formats.dump
DELETED
|
Binary file
|