addressing 2.0.1 → 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.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -0
  3. data/README.md +223 -111
  4. data/data/UPSTREAM_VERSION +1 -0
  5. data/data/address_formats.json +2256 -206
  6. data/data/countries.json +1282 -0
  7. data/data/locale.json +222 -0
  8. data/data/subdivision/AE.json +1 -1
  9. data/data/subdivision/BR-AC.json +22 -22
  10. data/data/subdivision/BR-AL.json +102 -102
  11. data/data/subdivision/BR-AM.json +62 -62
  12. data/data/subdivision/BR-AP.json +16 -16
  13. data/data/subdivision/BR-BA.json +417 -417
  14. data/data/subdivision/BR-CE.json +184 -184
  15. data/data/subdivision/BR-DF.json +1 -1
  16. data/data/subdivision/BR-ES.json +79 -79
  17. data/data/subdivision/BR-GO.json +246 -246
  18. data/data/subdivision/BR-MA.json +217 -217
  19. data/data/subdivision/BR-MG.json +853 -853
  20. data/data/subdivision/BR-MS.json +78 -78
  21. data/data/subdivision/BR-MT.json +141 -141
  22. data/data/subdivision/BR-PA.json +144 -144
  23. data/data/subdivision/BR-PB.json +223 -223
  24. data/data/subdivision/BR-PE.json +185 -185
  25. data/data/subdivision/BR-PI.json +223 -223
  26. data/data/subdivision/BR-PR.json +400 -400
  27. data/data/subdivision/BR-RJ.json +93 -93
  28. data/data/subdivision/BR-RN.json +166 -166
  29. data/data/subdivision/BR-RO.json +52 -52
  30. data/data/subdivision/BR-RR.json +15 -15
  31. data/data/subdivision/BR-RS.json +497 -497
  32. data/data/subdivision/BR-SC.json +295 -295
  33. data/data/subdivision/BR-SE.json +75 -75
  34. data/data/subdivision/BR-SP.json +645 -645
  35. data/data/subdivision/BR-TO.json +139 -139
  36. data/data/subdivision/CA.json +6 -12
  37. data/data/subdivision/CL-AI.json +10 -10
  38. data/data/subdivision/CL-AN.json +9 -9
  39. data/data/subdivision/CL-AP.json +4 -4
  40. data/data/subdivision/CL-AR.json +32 -32
  41. data/data/subdivision/CL-AT.json +9 -9
  42. data/data/subdivision/CL-BI.json +34 -34
  43. data/data/subdivision/CL-CO.json +15 -15
  44. data/data/subdivision/CL-LI.json +33 -33
  45. data/data/subdivision/CL-LL.json +30 -30
  46. data/data/subdivision/CL-LR.json +12 -12
  47. data/data/subdivision/CL-MA.json +11 -11
  48. data/data/subdivision/CL-ML.json +30 -30
  49. data/data/subdivision/CL-NB.json +21 -21
  50. data/data/subdivision/CL-RM.json +52 -52
  51. data/data/subdivision/CL-TA.json +7 -7
  52. data/data/subdivision/CL-VS.json +38 -38
  53. data/data/subdivision/CO.json +1 -1
  54. data/data/subdivision/CV.json +9 -9
  55. data/data/subdivision/GB.json +675 -0
  56. data/data/subdivision/GT.json +71 -0
  57. data/data/subdivision/IT.json +13 -1
  58. data/data/subdivision/KY.json +3 -3
  59. data/data/subdivision/NG.json +1 -1
  60. data/data/subdivision/RU.json +4 -4
  61. data/data/subdivision/TH.json +7 -7
  62. data/data/subdivision/TR.json +1 -1
  63. data/data/subdivision/TV.json +1 -1
  64. data/data/subdivision/VE.json +4 -4
  65. data/data/subdivision/VN.json +1 -1
  66. data/lib/addressing/address.rb +7 -3
  67. data/lib/addressing/address_format.rb +93 -97
  68. data/lib/addressing/address_validator.rb +98 -0
  69. data/lib/addressing/blank.rb +32 -0
  70. data/lib/addressing/country.rb +28 -314
  71. data/lib/addressing/data_source.rb +75 -0
  72. data/lib/addressing/default_formatter.rb +61 -33
  73. data/lib/addressing/enum.rb +2 -6
  74. data/lib/addressing/field_violation.rb +17 -0
  75. data/lib/addressing/lazy_subdivisions.rb +16 -15
  76. data/lib/addressing/locale.rb +20 -269
  77. data/lib/addressing/model.rb +20 -79
  78. data/lib/addressing/postal_label_formatter.rb +16 -15
  79. data/lib/addressing/subdivision.rb +121 -106
  80. data/lib/addressing/version.rb +1 -1
  81. data/lib/addressing.rb +6 -1
  82. metadata +11 -3
  83. 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
- create_subdivision_from_definitions(id, definitions)
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
- definitions["subdivisions"].each_with_object({}) do |(id, definition), subdivisions|
39
- subdivisions[id] = create_subdivision_from_definitions(id, definitions)
40
- end
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,71 +105,65 @@ module Addressing
47
105
 
48
106
  use_local_name = Locale.match_candidates(locale, definitions["locale"] || "")
49
107
 
50
- definitions["subdivisions"].each_with_object({}) do |(id, definition), 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
- protected
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?
56
121
 
57
- # Checks whether predefined subdivisions exist for the provided parents.
58
- def has_data(parents)
59
- country_code = parents[0]
60
-
61
- depth = AddressFormat.get(country_code).subdivision_depth
62
- return false if depth == 0
63
-
64
- if parents.size > 1
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 guessing based on depth.
78
- return parents.size <= depth
79
- end
80
- end
122
+ # Country codes are matched case-insensitively, subdivision IDs are not.
123
+ country_code = parents[0].upcase
124
+ subdivision_ids = parents.drop(1)
81
125
 
82
- # The first level has always data.
83
- true
84
- end
126
+ return country_code if subdivision_ids.empty?
85
127
 
86
- # Loads the subdivision definitions for the provided parents.
87
- def load_definitions(parents)
88
- @definitions ||= {}
89
- group = build_group(parents.dup)
90
- if @definitions.key?(group)
91
- return @definitions[group]
92
- end
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
93
130
 
94
- @definitions[group] = {}
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
95
135
 
96
- # If there are predefined subdivisions at this level, try to load them.
97
- if has_data(parents)
98
- filename = File.join(File.expand_path("../../../data/subdivision", __FILE__).to_s, "#{group}.json")
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"]]
144
+ end
99
145
 
100
- if File.exist?(filename)
101
- raw_definition = File.read(filename)
102
- @definitions[group] = JSON.parse(raw_definition)
103
- @definitions[group] = process_definitions(@definitions[group])
104
- end
105
- end
146
+ protected
106
147
 
107
- @definitions[group]
148
+ # Loads the subdivision definitions for the provided parents.
149
+ #
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) } || {}
108
153
  end
109
154
 
110
155
  # Processes the loaded definitions.
111
156
  #
112
157
  # Adds keys and values that were removed from the JSON files for brevity.
113
158
  def process_definitions(definitions)
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)
162
+
114
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
+
115
167
  # Add common keys from the root level.
116
168
  definition["country_code"] = definitions["country_code"]
117
169
  definition["id"] = id
@@ -124,9 +176,15 @@ module Addressing
124
176
  definition["name"] = id
125
177
  end
126
178
 
179
+ # The local_name value is only specified if it doesn't match
180
+ # the name one.
181
+ if definitions.key?("locale") && !definition.key?("local_name")
182
+ definition["local_name"] = definition["name"]
183
+ end
184
+
127
185
  # The code and local_code values are only specified if they
128
186
  # don't match the name and local_name ones.
129
- if !definition.key?("code") && definition.key?("name")
187
+ if !definition.key?("code")
130
188
  definition["code"] = definition["name"]
131
189
  end
132
190
 
@@ -138,66 +196,23 @@ module Addressing
138
196
  definitions
139
197
  end
140
198
 
141
- # Builds a group from the provided parents.
142
- #
143
- # Used for storing a country's subdivisions of a specific level.
144
- def build_group(parents)
145
- raise ArgumentError, "The parents argument must not be empty." if parents.empty?
146
-
147
- return parents[0] if parents.length == 1
148
-
149
- # The second parent is an ISO code, it can be used as-is.
150
- return parents.join("-") if parents.length == 2 && parents[1].length <= 3
151
-
152
- country_code = parents.shift
153
- group = country_code
154
-
155
- # A dash per key allows the depth to be guessed later.
156
- group += "-" * parents.length
157
- # Hash the remaining keys to ensure that the group is ASCII safe.
158
- 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
159
203
  end
160
204
 
161
205
  # Creates a subdivision object from the provided definitions.
162
- def create_subdivision_from_definitions(id, definitions)
163
- if !definitions.dig("subdivisions", id)
164
- # No matching definition found.
165
- return nil
166
- end
167
-
206
+ def create_subdivision_from_definitions(id, definitions, parent)
168
207
  definition = definitions["subdivisions"][id]
169
- # The 'parents' key is omitted when it contains just the country code.
170
- definitions["parents"] = [definitions["country_code"]] unless definitions.key?("parents")
171
- parents = definitions["parents"]
172
-
173
- definition["parent"] = nil
174
-
175
- # Load the parent, if known.
176
- if parents.size > 1
177
- grandparents = parents.dup
178
- parent_id = grandparents.pop
179
- parent_group = build_group(grandparents.dup)
180
- @parents ||= {}
181
-
182
- if !@parents.dig(parent_group, parent_id)
183
- @parents[parent_group] ||= {}
184
- @parents[parent_group][parent_id] = get(parent_id, grandparents)
185
- end
186
-
187
- definition["parent"] = @parents[parent_group][parent_id]
188
- end
208
+ parents = parents_of(definitions)
189
209
 
190
210
  # Prepare children.
191
- if definition["has_children"]
192
- children_parents = parents.dup
193
- children_parents << id
194
-
195
- definition["children"] = LazySubdivisions.new(children_parents)
196
- end
211
+ children = definition["has_children"] ? LazySubdivisions.new(parents + [id]) : {}
197
212
 
198
213
  new(
199
214
  id: id,
200
- parent: definition["parent"],
215
+ parent: parent,
201
216
  country_code: definition["country_code"],
202
217
  locale: definition["locale"],
203
218
  code: definition["code"],
@@ -205,7 +220,7 @@ module Addressing
205
220
  name: definition["name"],
206
221
  local_name: definition["local_name"],
207
222
  postal_code_pattern: definition["postal_code_pattern"],
208
- children: definition["children"] || {}
223
+ children: children
209
224
  )
210
225
  end
211
226
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Addressing
4
- VERSION = "2.0.1"
4
+ VERSION = "2.2.0"
5
5
  end
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.0.1
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-05-27 00:00:00.000000000 Z
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/address_formats.dump
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
@@ -617,6 +619,8 @@ files:
617
619
  - data/subdivision/EG.json
618
620
  - data/subdivision/ES.json
619
621
  - data/subdivision/FM.json
622
+ - data/subdivision/GB.json
623
+ - data/subdivision/GT.json
620
624
  - data/subdivision/HK-2705954f46a43ef0ad522412afe6ed6ee8196fe4.json
621
625
  - data/subdivision/HK-29ceaf41d01206ba481b188116cbb302b82663cf.json
622
626
  - data/subdivision/HK-ff51a84233701e6a1d18f70acf4b3f909e4c6a09.json
@@ -717,13 +721,17 @@ files:
717
721
  - lib/addressing/address.rb
718
722
  - lib/addressing/address_field.rb
719
723
  - lib/addressing/address_format.rb
724
+ - lib/addressing/address_validator.rb
720
725
  - lib/addressing/administrative_area_type.rb
726
+ - lib/addressing/blank.rb
721
727
  - lib/addressing/country.rb
728
+ - lib/addressing/data_source.rb
722
729
  - lib/addressing/default_formatter.rb
723
730
  - lib/addressing/dependent_locality_type.rb
724
731
  - lib/addressing/enum.rb
725
732
  - lib/addressing/exceptions.rb
726
733
  - lib/addressing/field_override.rb
734
+ - lib/addressing/field_violation.rb
727
735
  - lib/addressing/lazy_subdivisions.rb
728
736
  - lib/addressing/locale.rb
729
737
  - lib/addressing/locality_type.rb
Binary file