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.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -0
  3. data/README.md +223 -112
  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/BR-AC.json +22 -22
  9. data/data/subdivision/BR-AL.json +102 -102
  10. data/data/subdivision/BR-AM.json +62 -62
  11. data/data/subdivision/BR-AP.json +16 -16
  12. data/data/subdivision/BR-BA.json +417 -417
  13. data/data/subdivision/BR-CE.json +184 -184
  14. data/data/subdivision/BR-DF.json +1 -1
  15. data/data/subdivision/BR-ES.json +79 -79
  16. data/data/subdivision/BR-GO.json +246 -246
  17. data/data/subdivision/BR-MA.json +217 -217
  18. data/data/subdivision/BR-MG.json +853 -853
  19. data/data/subdivision/BR-MS.json +78 -78
  20. data/data/subdivision/BR-MT.json +141 -141
  21. data/data/subdivision/BR-PA.json +144 -144
  22. data/data/subdivision/BR-PB.json +223 -223
  23. data/data/subdivision/BR-PE.json +185 -185
  24. data/data/subdivision/BR-PI.json +223 -223
  25. data/data/subdivision/BR-PR.json +400 -400
  26. data/data/subdivision/BR-RJ.json +93 -93
  27. data/data/subdivision/BR-RN.json +166 -166
  28. data/data/subdivision/BR-RO.json +52 -52
  29. data/data/subdivision/BR-RR.json +15 -15
  30. data/data/subdivision/BR-RS.json +497 -497
  31. data/data/subdivision/BR-SC.json +295 -295
  32. data/data/subdivision/BR-SE.json +75 -75
  33. data/data/subdivision/BR-SP.json +645 -645
  34. data/data/subdivision/BR-TO.json +139 -139
  35. data/data/subdivision/CL-AI.json +10 -10
  36. data/data/subdivision/CL-AN.json +9 -9
  37. data/data/subdivision/CL-AP.json +4 -4
  38. data/data/subdivision/CL-AR.json +32 -32
  39. data/data/subdivision/CL-AT.json +9 -9
  40. data/data/subdivision/CL-BI.json +34 -34
  41. data/data/subdivision/CL-CO.json +15 -15
  42. data/data/subdivision/CL-LI.json +33 -33
  43. data/data/subdivision/CL-LL.json +30 -30
  44. data/data/subdivision/CL-LR.json +12 -12
  45. data/data/subdivision/CL-MA.json +11 -11
  46. data/data/subdivision/CL-ML.json +30 -30
  47. data/data/subdivision/CL-NB.json +21 -21
  48. data/data/subdivision/CL-RM.json +52 -52
  49. data/data/subdivision/CL-TA.json +7 -7
  50. data/data/subdivision/CL-VS.json +38 -38
  51. data/data/subdivision/CV.json +9 -9
  52. data/data/subdivision/KY.json +3 -3
  53. data/data/subdivision/TV.json +1 -1
  54. data/lib/addressing/address.rb +7 -3
  55. data/lib/addressing/address_format.rb +55 -81
  56. data/lib/addressing/address_validator.rb +98 -0
  57. data/lib/addressing/blank.rb +32 -0
  58. data/lib/addressing/country.rb +28 -314
  59. data/lib/addressing/data_source.rb +75 -0
  60. data/lib/addressing/default_formatter.rb +25 -25
  61. data/lib/addressing/enum.rb +2 -6
  62. data/lib/addressing/field_violation.rb +17 -0
  63. data/lib/addressing/lazy_subdivisions.rb +16 -15
  64. data/lib/addressing/locale.rb +20 -269
  65. data/lib/addressing/model.rb +20 -78
  66. data/lib/addressing/postal_label_formatter.rb +16 -15
  67. data/lib/addressing/subdivision.rb +113 -116
  68. data/lib/addressing/version.rb +1 -1
  69. data/lib/addressing.rb +6 -1
  70. metadata +9 -3
  71. 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,83 +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
56
-
57
- # Checks whether predefined subdivisions exist for the provided parents.
58
- def has_data(parents)
59
- country_code = parents[0]
60
-
61
- subdivision_fields = AddressFormat.get(country_code).subdivision_fields
62
- return false if subdivision_fields.empty?
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
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
- # Loads the subdivision definitions for the provided parents.
88
- def load_definitions(parents)
89
- @definitions ||= {}
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
- @definitions[group] = {}
126
+ return country_code if subdivision_ids.empty?
96
127
 
97
- # If there are predefined subdivisions at this level, try to load them.
98
- if has_data(parents)
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
- if File.exist?(filename)
102
- @definitions[group] = process_definitions(parse_definitions(File.read(filename)))
103
- end
104
- end
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
- @definitions[group]
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
- # Parses a raw definition file.
146
+ protected
147
+
148
+ # Loads the subdivision definitions for the provided parents.
110
149
  #
111
- # Malformed JSON is treated as if the file didn't exist.
112
- def parse_definitions(raw_definition)
113
- definitions = JSON.parse(raw_definition)
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
- # Malformed definitions are treated as if they didn't exist.
124
- return {} unless definitions["subdivisions"].is_a?(Hash)
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") && definition.key?("name")
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
- # Builds a group from the provided parents.
160
- #
161
- # Used for storing a country's subdivisions of a specific level.
162
- def build_group(parents)
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
- # The 'parents' key is omitted when it contains just the country code.
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
- if definition["has_children"]
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: definition["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: definition["children"] || {}
223
+ children: children
227
224
  )
228
225
  end
229
226
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Addressing
4
- VERSION = "2.1.0"
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.1.0
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-09-18 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
@@ -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
Binary file