atlas_engine 0.1.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 (298) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +123 -0
  3. data/Rakefile +20 -0
  4. data/app/assets/config/atlas_engine_manifest.js +3 -0
  5. data/app/assets/stylesheets/atlas_engine/application.css +15 -0
  6. data/app/concerns/atlas_engine/handles_blob.rb +26 -0
  7. data/app/concerns/atlas_engine/handles_interruption.rb +22 -0
  8. data/app/controllers/atlas_engine/application_controller.rb +7 -0
  9. data/app/controllers/atlas_engine/connectivity_controller.rb +21 -0
  10. data/app/controllers/atlas_engine/country_imports_controller.rb +73 -0
  11. data/app/controllers/atlas_engine/graphql_controller.rb +59 -0
  12. data/app/countries/atlas_engine/ar/country_profile.yml +9 -0
  13. data/app/countries/atlas_engine/at/address_importer/corrections/open_address/city_corrector.rb +23 -0
  14. data/app/countries/atlas_engine/at/country_profile.yml +24 -0
  15. data/app/countries/atlas_engine/at/index_configuration.yml +63 -0
  16. data/app/countries/atlas_engine/at/synonyms.yml +6 -0
  17. data/app/countries/atlas_engine/at/validation_transcriber/address_parser.rb +58 -0
  18. data/app/countries/atlas_engine/au/address_importer/open_address/filter.rb +26 -0
  19. data/app/countries/atlas_engine/au/address_importer/open_address/mapper.rb +41 -0
  20. data/app/countries/atlas_engine/au/country_profile.yml +13 -0
  21. data/app/countries/atlas_engine/au/synonyms.yml +209 -0
  22. data/app/countries/atlas_engine/au/validation_transcriber/address_parser.rb +121 -0
  23. data/app/countries/atlas_engine/be/country_profile.yml +12 -0
  24. data/app/countries/atlas_engine/bm/address_importer/corrections/open_address/city_alias_corrector.rb +38 -0
  25. data/app/countries/atlas_engine/bm/address_importer/open_address/mapper.rb +40 -0
  26. data/app/countries/atlas_engine/bm/country_profile.yml +12 -0
  27. data/app/countries/atlas_engine/br/country_profile.yml +4 -0
  28. data/app/countries/atlas_engine/ca/country_profile.yml +7 -0
  29. data/app/countries/atlas_engine/ca/synonyms.yml +1615 -0
  30. data/app/countries/atlas_engine/ch/address_importer/corrections/open_address/city_corrector.rb +29 -0
  31. data/app/countries/atlas_engine/ch/address_importer/corrections/open_address/locale_corrector.rb +74 -0
  32. data/app/countries/atlas_engine/ch/address_importer/open_address/mapper.rb +40 -0
  33. data/app/countries/atlas_engine/ch/country_profile.yml +15 -0
  34. data/app/countries/atlas_engine/ch/locales/de/country_profile.yml +15 -0
  35. data/app/countries/atlas_engine/ch/locales/de/index_configuration.yml +63 -0
  36. data/app/countries/atlas_engine/ch/locales/de/synonyms.yml +7 -0
  37. data/app/countries/atlas_engine/ch/locales/fr/synonyms.yml +21 -0
  38. data/app/countries/atlas_engine/cz/country_profile.yml +6 -0
  39. data/app/countries/atlas_engine/de/country_profile.yml +19 -0
  40. data/app/countries/atlas_engine/de/index_configuration.yml +64 -0
  41. data/app/countries/atlas_engine/de/synonyms.yml +2 -0
  42. data/app/countries/atlas_engine/de/validation_transcriber/address_parser.rb +19 -0
  43. data/app/countries/atlas_engine/dk/country_profile.yml +6 -0
  44. data/app/countries/atlas_engine/dk/synonyms.yml +3 -0
  45. data/app/countries/atlas_engine/dk/validation_transcriber/address_parser.rb +21 -0
  46. data/app/countries/atlas_engine/fo/country_profile.yml +5 -0
  47. data/app/countries/atlas_engine/fr/address_importer/corrections/open_address/city_corrector.rb +28 -0
  48. data/app/countries/atlas_engine/fr/country_profile.yml +13 -0
  49. data/app/countries/atlas_engine/fr/synonyms.yml +21 -0
  50. data/app/countries/atlas_engine/fr/validation_transcriber/address_parser.rb +34 -0
  51. data/app/countries/atlas_engine/gb/address_validation/es/query_builder.rb +98 -0
  52. data/app/countries/atlas_engine/gb/country_profile.yml +10 -0
  53. data/app/countries/atlas_engine/gb/validation_transcriber/full_address_parser.rb +164 -0
  54. data/app/countries/atlas_engine/gb/validation_transcriber/parsed_address.rb +120 -0
  55. data/app/countries/atlas_engine/gg/address_validation/validators/full_address/restrictions/unsupported_city.rb +39 -0
  56. data/app/countries/atlas_engine/gg/country_profile.yml +7 -0
  57. data/app/countries/atlas_engine/ie/country_profile.yml +3 -0
  58. data/app/countries/atlas_engine/it/address_importer/corrections/open_address/city_corrector.rb +27 -0
  59. data/app/countries/atlas_engine/it/address_importer/corrections/open_address/province_corrector.rb +29 -0
  60. data/app/countries/atlas_engine/it/address_importer/open_address/mapper.rb +42 -0
  61. data/app/countries/atlas_engine/it/country_profile.yml +11 -0
  62. data/app/countries/atlas_engine/jp/address_validation/es/data_mapper.rb +63 -0
  63. data/app/countries/atlas_engine/jp/country_profile.yml +6 -0
  64. data/app/countries/atlas_engine/kr/address_importer/open_address/mapper.rb +41 -0
  65. data/app/countries/atlas_engine/kr/country_profile.yml +11 -0
  66. data/app/countries/atlas_engine/li/address_importer/corrections/open_address/city_corrector.rb +25 -0
  67. data/app/countries/atlas_engine/li/country_profile.yml +21 -0
  68. data/app/countries/atlas_engine/li/index_configuration.yml +63 -0
  69. data/app/countries/atlas_engine/li/synonyms.yml +6 -0
  70. data/app/countries/atlas_engine/lt/country_profile.yml +6 -0
  71. data/app/countries/atlas_engine/lt/synonyms.yml +7 -0
  72. data/app/countries/atlas_engine/lt/validation_transcriber/address_parser.rb +24 -0
  73. data/app/countries/atlas_engine/lu/address_importer/corrections/open_address/locale_corrector.rb +54 -0
  74. data/app/countries/atlas_engine/lu/country_profile.yml +12 -0
  75. data/app/countries/atlas_engine/nl/address_importer/corrections/open_address/city_corrector.rb +25 -0
  76. data/app/countries/atlas_engine/nl/country_profile.yml +18 -0
  77. data/app/countries/atlas_engine/nl/index_configuration.yml +52 -0
  78. data/app/countries/atlas_engine/nl/synonyms.yml +92 -0
  79. data/app/countries/atlas_engine/nl/validation_transcriber/address_parser.rb +85 -0
  80. data/app/countries/atlas_engine/no/country_profile.yml +5 -0
  81. data/app/countries/atlas_engine/nz/country_profile.yml +3 -0
  82. data/app/countries/atlas_engine/pl/country_profile.yml +5 -0
  83. data/app/countries/atlas_engine/pl/validation_transcriber/address_parser.rb +19 -0
  84. data/app/countries/atlas_engine/pt/address_importer/corrections/open_address/city_corrector.rb +32 -0
  85. data/app/countries/atlas_engine/pt/address_importer/open_address/mapper.rb +39 -0
  86. data/app/countries/atlas_engine/pt/country_profile.yml +10 -0
  87. data/app/countries/atlas_engine/pt/synonyms.yml +7 -0
  88. data/app/countries/atlas_engine/sa/country_profile.yml +10 -0
  89. data/app/countries/atlas_engine/se/country_profile.yml +5 -0
  90. data/app/countries/atlas_engine/tt/address_importer/open_address/mapper.rb +38 -0
  91. data/app/countries/atlas_engine/tt/country_profile.yml +7 -0
  92. data/app/countries/atlas_engine/us/country_profile.yml +12 -0
  93. data/app/countries/atlas_engine/us/synonyms.yml +350 -0
  94. data/app/graphql/atlas_engine/errors/locale_unsupported_error.rb +17 -0
  95. data/app/graphql/atlas_engine/schema.graphql +1293 -0
  96. data/app/graphql/atlas_engine/schema.rb +23 -0
  97. data/app/graphql/atlas_engine/types/address_validation/address_input.rb +51 -0
  98. data/app/graphql/atlas_engine/types/address_validation/concern_type.rb +20 -0
  99. data/app/graphql/atlas_engine/types/address_validation/enums/concern_enum.rb +15 -0
  100. data/app/graphql/atlas_engine/types/address_validation/field_type.rb +15 -0
  101. data/app/graphql/atlas_engine/types/address_validation/suggestion_type.rb +21 -0
  102. data/app/graphql/atlas_engine/types/base_argument.rb +9 -0
  103. data/app/graphql/atlas_engine/types/base_enum.rb +9 -0
  104. data/app/graphql/atlas_engine/types/base_field.rb +10 -0
  105. data/app/graphql/atlas_engine/types/base_input_object.rb +9 -0
  106. data/app/graphql/atlas_engine/types/base_interface.rb +10 -0
  107. data/app/graphql/atlas_engine/types/base_object.rb +9 -0
  108. data/app/graphql/atlas_engine/types/base_scalar.rb +9 -0
  109. data/app/graphql/atlas_engine/types/base_union.rb +9 -0
  110. data/app/graphql/atlas_engine/types/matching_strategy_type.rb +12 -0
  111. data/app/graphql/atlas_engine/types/mutation_type.rb +9 -0
  112. data/app/graphql/atlas_engine/types/query_type.rb +61 -0
  113. data/app/graphql/atlas_engine/types/validation_supported_country.rb +12 -0
  114. data/app/graphql/atlas_engine/types/validation_type.rb +22 -0
  115. data/app/helpers/atlas_engine/address_importer/import_log_helper.rb +66 -0
  116. data/app/helpers/atlas_engine/application_helper.rb +7 -0
  117. data/app/helpers/atlas_engine/locale_format_helper.rb +40 -0
  118. data/app/helpers/atlas_engine/log_base.rb +32 -0
  119. data/app/helpers/atlas_engine/log_helper.rb +24 -0
  120. data/app/helpers/atlas_engine/metrics_helper.rb +25 -0
  121. data/app/jobs/atlas_engine/address_importer/clear_records_job.rb +39 -0
  122. data/app/jobs/atlas_engine/address_importer/open_address/geo_json_import_job.rb +212 -0
  123. data/app/jobs/atlas_engine/address_importer/open_address/geo_json_import_launcher_job.rb +67 -0
  124. data/app/jobs/atlas_engine/address_importer/open_address/prepares_geo_json_file.rb +41 -0
  125. data/app/jobs/atlas_engine/address_importer/resumable_import_job.rb +49 -0
  126. data/app/jobs/atlas_engine/address_importer/street_backfill_job.rb +63 -0
  127. data/app/jobs/atlas_engine/application_job.rb +10 -0
  128. data/app/jobs/atlas_engine/concerns/address_importer/handles_errors.rb +43 -0
  129. data/app/lib/atlas_engine/concern_formatter.rb +40 -0
  130. data/app/lib/atlas_engine/restrictions/base.rb +20 -0
  131. data/app/lib/atlas_engine/restrictions/unsupported_script.rb +31 -0
  132. data/app/lib/atlas_engine/validation_transcriber/address_parser_base.rb +201 -0
  133. data/app/lib/atlas_engine/validation_transcriber/address_parser_factory.rb +27 -0
  134. data/app/lib/atlas_engine/validation_transcriber/address_parser_north_america.rb +39 -0
  135. data/app/lib/atlas_engine/validation_transcriber/address_parser_oceanic.rb +17 -0
  136. data/app/lib/atlas_engine/validation_transcriber/address_parser_preprocessor.rb +132 -0
  137. data/app/lib/atlas_engine/validation_transcriber/address_parsing_helper.rb +38 -0
  138. data/app/lib/atlas_engine/validation_transcriber/address_parsings.rb +54 -0
  139. data/app/lib/atlas_engine/validation_transcriber/constants.rb +50 -0
  140. data/app/lib/atlas_engine/validation_transcriber/english_street_parser.rb +59 -0
  141. data/app/lib/atlas_engine/validation_transcriber/formatter.rb +46 -0
  142. data/app/lib/atlas_engine/validation_transcriber/french_street_parser.rb +50 -0
  143. data/app/lib/atlas_engine/validation_transcriber/province_code_normalizer.rb +45 -0
  144. data/app/lib/atlas_engine/validation_transcriber/street_parser.rb +18 -0
  145. data/app/lib/atlas_engine/validation_transcriber/zip_normalizer.rb +23 -0
  146. data/app/mailers/atlas_engine/application_mailer.rb +9 -0
  147. data/app/models/atlas_engine/address_importer/corrections/corrector.rb +33 -0
  148. data/app/models/atlas_engine/address_importer/import_events_notifier/base.rb +35 -0
  149. data/app/models/atlas_engine/address_importer/import_events_notifier/notifier.rb +26 -0
  150. data/app/models/atlas_engine/address_importer/open_address/default_mapper.rb +46 -0
  151. data/app/models/atlas_engine/address_importer/open_address/feature_helper.rb +110 -0
  152. data/app/models/atlas_engine/address_importer/open_address/filter.rb +17 -0
  153. data/app/models/atlas_engine/address_importer/open_address/loader.rb +27 -0
  154. data/app/models/atlas_engine/address_importer/open_address/transformer.rb +39 -0
  155. data/app/models/atlas_engine/address_importer/open_address.rb +10 -0
  156. data/app/models/atlas_engine/address_importer/validation/base_validator.rb +86 -0
  157. data/app/models/atlas_engine/address_importer/validation/default_validator.rb +27 -0
  158. data/app/models/atlas_engine/address_importer/validation/field_validations/city.rb +47 -0
  159. data/app/models/atlas_engine/address_importer/validation/field_validations/interface.rb +29 -0
  160. data/app/models/atlas_engine/address_importer/validation/field_validations/province.rb +73 -0
  161. data/app/models/atlas_engine/address_importer/validation/field_validations/zip.rb +84 -0
  162. data/app/models/atlas_engine/address_importer/validation/validator.rb +17 -0
  163. data/app/models/atlas_engine/address_importer/validation/wrapper.rb +70 -0
  164. data/app/models/atlas_engine/address_number.rb +36 -0
  165. data/app/models/atlas_engine/address_number_range.rb +200 -0
  166. data/app/models/atlas_engine/address_validation/abstract_address.rb +49 -0
  167. data/app/models/atlas_engine/address_validation/address.rb +47 -0
  168. data/app/models/atlas_engine/address_validation/candidate.rb +109 -0
  169. data/app/models/atlas_engine/address_validation/candidate_tuple.rb +15 -0
  170. data/app/models/atlas_engine/address_validation/concern.rb +74 -0
  171. data/app/models/atlas_engine/address_validation/concern_producer.rb +19 -0
  172. data/app/models/atlas_engine/address_validation/concern_queue.rb +20 -0
  173. data/app/models/atlas_engine/address_validation/concern_record.rb +122 -0
  174. data/app/models/atlas_engine/address_validation/datastore_base.rb +27 -0
  175. data/app/models/atlas_engine/address_validation/errors.rb +13 -0
  176. data/app/models/atlas_engine/address_validation/es/candidate_selector.rb +70 -0
  177. data/app/models/atlas_engine/address_validation/es/data_mappers/decompounding_data_mapper.rb +39 -0
  178. data/app/models/atlas_engine/address_validation/es/data_mappers/default_data_mapper.rb +110 -0
  179. data/app/models/atlas_engine/address_validation/es/datastore.rb +229 -0
  180. data/app/models/atlas_engine/address_validation/es/default_query_builder.rb +30 -0
  181. data/app/models/atlas_engine/address_validation/es/query_builder.rb +160 -0
  182. data/app/models/atlas_engine/address_validation/es/term_vectors.rb +78 -0
  183. data/app/models/atlas_engine/address_validation/es/validators/full_address.rb +123 -0
  184. data/app/models/atlas_engine/address_validation/es/validators/full_address_street.rb +18 -0
  185. data/app/models/atlas_engine/address_validation/es/validators/restriction_evaluator.rb +37 -0
  186. data/app/models/atlas_engine/address_validation/field.rb +30 -0
  187. data/app/models/atlas_engine/address_validation/full_address_validator_base.rb +27 -0
  188. data/app/models/atlas_engine/address_validation/log_emitter.rb +66 -0
  189. data/app/models/atlas_engine/address_validation/matching_strategies.rb +16 -0
  190. data/app/models/atlas_engine/address_validation/normalizer.rb +38 -0
  191. data/app/models/atlas_engine/address_validation/predicate_pipeline.rb +80 -0
  192. data/app/models/atlas_engine/address_validation/request.rb +12 -0
  193. data/app/models/atlas_engine/address_validation/result.rb +154 -0
  194. data/app/models/atlas_engine/address_validation/runs_validation.rb +16 -0
  195. data/app/models/atlas_engine/address_validation/session.rb +47 -0
  196. data/app/models/atlas_engine/address_validation/statsd_emitter.rb +72 -0
  197. data/app/models/atlas_engine/address_validation/strategies.rb +10 -0
  198. data/app/models/atlas_engine/address_validation/suggestion.rb +97 -0
  199. data/app/models/atlas_engine/address_validation/token/comparator.rb +44 -0
  200. data/app/models/atlas_engine/address_validation/token/comparison.rb +76 -0
  201. data/app/models/atlas_engine/address_validation/token/sequence/comparator.rb +158 -0
  202. data/app/models/atlas_engine/address_validation/token/sequence/comparison.rb +166 -0
  203. data/app/models/atlas_engine/address_validation/token/sequence.rb +147 -0
  204. data/app/models/atlas_engine/address_validation/token/synonyms.rb +77 -0
  205. data/app/models/atlas_engine/address_validation/token.rb +113 -0
  206. data/app/models/atlas_engine/address_validation/validator.rb +147 -0
  207. data/app/models/atlas_engine/address_validation/validators/full_address/address_comparison.rb +97 -0
  208. data/app/models/atlas_engine/address_validation/validators/full_address/candidate_result.rb +164 -0
  209. data/app/models/atlas_engine/address_validation/validators/full_address/candidate_result_base.rb +46 -0
  210. data/app/models/atlas_engine/address_validation/validators/full_address/comparison_helper.rb +135 -0
  211. data/app/models/atlas_engine/address_validation/validators/full_address/components_to_validate.rb +88 -0
  212. data/app/models/atlas_engine/address_validation/validators/full_address/concern_builder.rb +127 -0
  213. data/app/models/atlas_engine/address_validation/validators/full_address/exclusions/exclusion_base.rb +23 -0
  214. data/app/models/atlas_engine/address_validation/validators/full_address/invalid_zip_concern_builder.rb +42 -0
  215. data/app/models/atlas_engine/address_validation/validators/full_address/invalid_zip_for_country_concern.rb +37 -0
  216. data/app/models/atlas_engine/address_validation/validators/full_address/invalid_zip_for_province_concern.rb +37 -0
  217. data/app/models/atlas_engine/address_validation/validators/full_address/no_candidate_result.rb +26 -0
  218. data/app/models/atlas_engine/address_validation/validators/full_address/number_comparison.rb +31 -0
  219. data/app/models/atlas_engine/address_validation/validators/full_address/postal_code_matcher.rb +60 -0
  220. data/app/models/atlas_engine/address_validation/validators/full_address/result_updater.rb +42 -0
  221. data/app/models/atlas_engine/address_validation/validators/full_address/suggestion_builder.rb +140 -0
  222. data/app/models/atlas_engine/address_validation/validators/full_address/unknown_address_concern.rb +30 -0
  223. data/app/models/atlas_engine/address_validation/validators/full_address/unknown_province_concern.rb +38 -0
  224. data/app/models/atlas_engine/address_validation/validators/full_address/unknown_zip_for_address_concern.rb +32 -0
  225. data/app/models/atlas_engine/address_validation/validators/full_address/unmatched_field_concern.rb +84 -0
  226. data/app/models/atlas_engine/address_validation/validators/full_address/unsupported_script_result.rb +22 -0
  227. data/app/models/atlas_engine/address_validation/validators/predicates/cache.rb +38 -0
  228. data/app/models/atlas_engine/address_validation/validators/predicates/city/present.rb +36 -0
  229. data/app/models/atlas_engine/address_validation/validators/predicates/country/exists.rb +34 -0
  230. data/app/models/atlas_engine/address_validation/validators/predicates/country/valid_for_zip.rb +60 -0
  231. data/app/models/atlas_engine/address_validation/validators/predicates/no_emojis.rb +38 -0
  232. data/app/models/atlas_engine/address_validation/validators/predicates/no_html_tags.rb +39 -0
  233. data/app/models/atlas_engine/address_validation/validators/predicates/no_url.rb +38 -0
  234. data/app/models/atlas_engine/address_validation/validators/predicates/not_exceed_max_length.rb +34 -0
  235. data/app/models/atlas_engine/address_validation/validators/predicates/not_exceed_max_token_count.rb +63 -0
  236. data/app/models/atlas_engine/address_validation/validators/predicates/phone/valid.rb +41 -0
  237. data/app/models/atlas_engine/address_validation/validators/predicates/predicate.rb +37 -0
  238. data/app/models/atlas_engine/address_validation/validators/predicates/province/exists.rb +43 -0
  239. data/app/models/atlas_engine/address_validation/validators/predicates/province/valid_for_country.rb +48 -0
  240. data/app/models/atlas_engine/address_validation/validators/predicates/street/building_number_in_address1.rb +45 -0
  241. data/app/models/atlas_engine/address_validation/validators/predicates/street/building_number_in_address1_or_address2.rb +43 -0
  242. data/app/models/atlas_engine/address_validation/validators/predicates/street/present.rb +35 -0
  243. data/app/models/atlas_engine/address_validation/validators/predicates/zip/present.rb +58 -0
  244. data/app/models/atlas_engine/address_validation/validators/predicates/zip/valid_for_country.rb +45 -0
  245. data/app/models/atlas_engine/address_validation/validators/predicates/zip/valid_for_province.rb +55 -0
  246. data/app/models/atlas_engine/address_validation/validators/predicates/zip/zip_base.rb +25 -0
  247. data/app/models/atlas_engine/address_validation/zip_truncator.rb +32 -0
  248. data/app/models/atlas_engine/application_record.rb +8 -0
  249. data/app/models/atlas_engine/coded_error.rb +18 -0
  250. data/app/models/atlas_engine/coded_errors.rb +17 -0
  251. data/app/models/atlas_engine/country_import.rb +44 -0
  252. data/app/models/atlas_engine/country_profile.rb +270 -0
  253. data/app/models/atlas_engine/country_profile_ingestion_subset.rb +42 -0
  254. data/app/models/atlas_engine/country_profile_subset_base.rb +22 -0
  255. data/app/models/atlas_engine/country_profile_validation_subset.rb +48 -0
  256. data/app/models/atlas_engine/country_repository.rb +110 -0
  257. data/app/models/atlas_engine/elasticsearch/client.rb +116 -0
  258. data/app/models/atlas_engine/elasticsearch/client_interface.rb +89 -0
  259. data/app/models/atlas_engine/elasticsearch/repository.rb +246 -0
  260. data/app/models/atlas_engine/elasticsearch/repository_interface.rb +82 -0
  261. data/app/models/atlas_engine/elasticsearch/response.rb +20 -0
  262. data/app/models/atlas_engine/event.rb +12 -0
  263. data/app/models/atlas_engine/field_decompounder.rb +36 -0
  264. data/app/models/atlas_engine/index_configuration_factory.rb +188 -0
  265. data/app/models/atlas_engine/post_address.rb +114 -0
  266. data/app/models/atlas_engine/post_address_importer.rb +34 -0
  267. data/app/models/atlas_engine/services/service_helper.rb +21 -0
  268. data/app/models/atlas_engine/services/validation.rb +65 -0
  269. data/app/models/atlas_engine/services/validation_eligibility.rb +18 -0
  270. data/app/models/atlas_engine/street.rb +34 -0
  271. data/app/tasks/maintenance/atlas_engine/elasticsearch_index_create_task.rb +106 -0
  272. data/app/tasks/maintenance/atlas_engine/geo_json_import_task.rb +29 -0
  273. data/app/views/atlas_engine/connectivity/index.html.erb +50 -0
  274. data/app/views/atlas_engine/country_imports/index.html.erb +49 -0
  275. data/app/views/atlas_engine/country_imports/show.html.erb +73 -0
  276. data/app/views/layouts/atlas_engine/application.html.erb +15 -0
  277. data/config/initializers/1.ruby_patches.rb +18 -0
  278. data/config/initializers/sorbet.rb +5 -0
  279. data/config/initializers/worldwide.rb +5 -0
  280. data/config/locales/internal/en.yml +14 -0
  281. data/config/routes.rb +17 -0
  282. data/db/data/address_synonyms/index_configurations/default.yml +141 -0
  283. data/db/data/country_profiles/default.yml +23 -0
  284. data/db/data/transcriber.yml +760 -0
  285. data/db/data/validation_pipelines/es.yml +58 -0
  286. data/db/data/validation_pipelines/es_street.yml +58 -0
  287. data/db/data/validation_pipelines/local.yml +60 -0
  288. data/db/migrate/20230919173037_create_atlas_engine_post_addresses.rb +25 -0
  289. data/db/migrate/20231117142735_add_building_and_unit_ranges_column.rb +7 -0
  290. data/db/migrate/20231117143536_create_atlas_engine_country_imports.rb +11 -0
  291. data/db/migrate/20231117145844_create_atlas_engine_events_table.rb +13 -0
  292. data/db/migrate/20231123153554_add_unique_index_to_atlas_engine_post_addresses.rb +14 -0
  293. data/db/migrate/20231123154658_add_index_to_post_addresses_on_source_id_locale_country_code.rb +12 -0
  294. data/lib/atlas_engine/engine.rb +10 -0
  295. data/lib/atlas_engine/version.rb +6 -0
  296. data/lib/atlas_engine.rb +66 -0
  297. data/lib/tasks/atlas_engine/address_importer.rake +20 -0
  298. metadata +553 -0
@@ -0,0 +1,246 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module AtlasEngine
5
+ module Elasticsearch
6
+ class Repository
7
+ include RepositoryInterface
8
+ extend T::Sig
9
+ extend T::Helpers
10
+
11
+ INITIAL_INDEX_VERSION = 0
12
+
13
+ sig { returns(ClientInterface) }
14
+ attr_reader :client
15
+
16
+ sig { returns(String) }
17
+ attr_reader :index_base_name
18
+
19
+ sig { returns(T::Hash[String, T.untyped]) }
20
+ attr_reader :index_mappings
21
+
22
+ sig { returns(T::Hash[Symbol, T.untyped]) }
23
+ attr_reader :index_settings
24
+
25
+ sig { returns(T.proc.params(arg0: T.untyped).returns(T.untyped)) }
26
+ attr_reader :mapper_callable
27
+
28
+ sig do
29
+ override.params(
30
+ index_base_name: String,
31
+ index_settings: T::Hash[Symbol, T.untyped],
32
+ index_mappings: T.nilable(T::Hash[String, T.untyped]),
33
+ mapper_callable: T.nilable(T.proc.params(arg0: T.untyped).returns(T.untyped)),
34
+ ).void
35
+ end
36
+ def initialize(index_base_name:, index_settings:, index_mappings:, mapper_callable: nil)
37
+ @client = T.let(Client.new, ClientInterface)
38
+ @index_base_name = T.let(index_base_name, String)
39
+ @index_mappings = T.let(index_mappings || default_mapping, T::Hash[String, T.untyped])
40
+ @index_settings = T.let(index_settings, T::Hash[Symbol, T.untyped])
41
+ @mapper_callable = T.let(
42
+ mapper_callable || ->(record) { record.to_hash },
43
+ T.proc.params(arg0: T.untyped).returns(T.untyped),
44
+ )
45
+ end
46
+
47
+ sig { override.returns(String) }
48
+ def base_alias_name
49
+ if Rails.env.test?
50
+ "test_#{index_base_name.to_s.downcase}"
51
+ else
52
+ index_base_name.to_s.downcase
53
+ end
54
+ end
55
+
56
+ sig { override.returns(String) }
57
+ def read_alias_name
58
+ base_alias_name
59
+ end
60
+
61
+ sig do
62
+ override.params(
63
+ ensure_clean: T::Boolean,
64
+ raise_errors: T::Boolean,
65
+ ).void
66
+ end
67
+ def create_next_index(ensure_clean: false, raise_errors: false)
68
+ # PENDING: cleanup next index if ensure_clean = true
69
+ return if client.index_or_alias_exists?(new_alias)
70
+
71
+ versioned_index_name = if client.index_or_alias_exists?(active_alias)
72
+ T.must(client.find_index_by(alias_name: active_alias)).next
73
+ else
74
+ "#{active_alias}.#{INITIAL_INDEX_VERSION}"
75
+ end
76
+
77
+ body = {
78
+ aliases: {
79
+ new_alias.to_s => {
80
+ is_write_index: true,
81
+ },
82
+ },
83
+ settings: index_settings,
84
+ mappings: index_mappings,
85
+ }
86
+
87
+ client.put(versioned_index_name, body)
88
+ rescue
89
+ raise if raise_errors
90
+ end
91
+
92
+ sig do
93
+ override.params(
94
+ raise_errors: T::Boolean,
95
+ ).void
96
+ end
97
+ def switch_to_next_index(raise_errors: false)
98
+ update_all_aliases_of_index if client.index_or_alias_exists?(new_alias)
99
+ rescue
100
+ raise if raise_errors
101
+ end
102
+
103
+ sig do
104
+ override.params(
105
+ records: T.any(ActiveRecord::Relation, T::Array[PostAddressData]),
106
+ ).returns(T.nilable(Response))
107
+ end
108
+ def save_records_backfill(records)
109
+ return if records.blank?
110
+
111
+ alias_name = if client.index_or_alias_exists?(new_alias)
112
+ new_alias
113
+ elsif client.index_or_alias_exists?(active_alias)
114
+ active_alias
115
+ else
116
+ raise "Next or current index must exist to backfill records"
117
+ end
118
+
119
+ body = ""
120
+ records.each do |record|
121
+ body += <<-NDJSON
122
+ { "create": {} }
123
+ #{record_source(record).to_json}
124
+ NDJSON
125
+ end
126
+
127
+ client.post("/#{alias_name}/_bulk", body)
128
+ end
129
+
130
+ sig { override.params(query: T::Hash[String, T.untyped]).returns(T::Hash[String, T.untyped]) }
131
+ def search(query)
132
+ path = "/#{active_alias}/_search"
133
+ response = client.post(path, query, {})
134
+
135
+ response.body
136
+ rescue ::Elastic::Transport::Transport::Error
137
+ { "hits" => { "hits" => [] } }
138
+ end
139
+
140
+ sig { override.params(query: T::Hash[String, T.untyped]).returns(T::Hash[String, T.untyped]) }
141
+ def analyze(query)
142
+ path = "/#{active_alias}/_analyze"
143
+ response = client.post(path, query, {})
144
+
145
+ response.body
146
+ rescue ::Elastic::Transport::Transport::Error
147
+ { "tokens" => [] }
148
+ end
149
+
150
+ sig { override.params(query: T::Hash[String, T.untyped]).returns(T::Hash[String, T.untyped]) }
151
+ def term_vectors(query)
152
+ path = "/#{active_alias}/_mtermvectors"
153
+ response = client.post(path, query, {})
154
+
155
+ response.body
156
+ rescue ::Elastic::Transport::Transport::Error
157
+ { "docs" => [] }
158
+ end
159
+
160
+ sig { override.params(id: T.any(String, Integer)).returns(T::Hash[String, T.untyped]) }
161
+ def find(id)
162
+ path = "/#{active_alias}/_doc/#{id}"
163
+ response = client.get(path, nil, {})
164
+
165
+ response.body["_source"]
166
+ rescue ::Elastic::Transport::Transport::Error
167
+ {}
168
+ end
169
+
170
+ sig { override.params(post_address: PostAddressData).returns(T::Hash[Symbol, T.untyped]) }
171
+ def record_source(post_address)
172
+ mapper_callable.call(post_address).compact
173
+ end
174
+
175
+ sig { returns(T::Array[T.untyped]) }
176
+ def indices
177
+ path = "/_cat/indices?format=json"
178
+ response = client.get(path, nil, {})
179
+
180
+ response.body
181
+ end
182
+
183
+ private
184
+
185
+ sig { returns(T::Hash[String, T.untyped]) }
186
+ def default_mapping
187
+ {
188
+ "dynamic" => "false",
189
+ "properties" => {
190
+ "id" => { "type" => "long" },
191
+ },
192
+ }
193
+ end
194
+
195
+ sig { void }
196
+ def update_all_aliases_of_index
197
+ previous_index_name = client.find_index_by(alias_name: archived_alias)
198
+ current_index_name = client.find_index_by(alias_name: active_alias)
199
+ next_index_name = client.find_index_by(alias_name: new_alias)
200
+
201
+ # delete the .archive alias
202
+ if previous_index_name.present?
203
+ client.delete(previous_index_name)
204
+ end
205
+
206
+ # archve the current alias
207
+ if current_index_name.present?
208
+ update_aliases_for_index(
209
+ index_name: current_index_name,
210
+ remove_alias: active_alias,
211
+ add_alias: archived_alias,
212
+ )
213
+ end
214
+
215
+ # activate the .new alias
216
+ if next_index_name.present?
217
+ update_aliases_for_index(
218
+ index_name: next_index_name,
219
+ remove_alias: new_alias,
220
+ add_alias: active_alias,
221
+ )
222
+ end
223
+ end
224
+
225
+ sig do
226
+ params(
227
+ index_name: String,
228
+ remove_alias: String,
229
+ add_alias: String,
230
+ ).void
231
+ end
232
+ def update_aliases_for_index(index_name:, remove_alias:, add_alias:)
233
+ is_writable = add_alias == active_alias ? true : false
234
+
235
+ body = {
236
+ actions: [
237
+ { remove: { index: index_name, alias: remove_alias } },
238
+ { add: { index: index_name, alias: add_alias, is_write_index: is_writable } },
239
+ ],
240
+ }
241
+
242
+ client.post("/_aliases", body)
243
+ end
244
+ end
245
+ end
246
+ end
@@ -0,0 +1,82 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module AtlasEngine
5
+ module Elasticsearch
6
+ module RepositoryInterface
7
+ extend T::Sig
8
+ extend T::Helpers
9
+ abstract!
10
+
11
+ PostAddressData = T.type_alias { T.any(PostAddress, T::Hash[Symbol, T.untyped]) }
12
+
13
+ sig do
14
+ abstract.params(
15
+ index_base_name: String,
16
+ index_settings: T::Hash[Symbol, T.untyped],
17
+ index_mappings: T.nilable(T::Hash[String, T.untyped]),
18
+ mapper_callable: T.nilable(T.proc.params(arg0: T.untyped).returns(T.untyped)),
19
+ ).void
20
+ end
21
+ def initialize(index_base_name:, index_settings:, index_mappings:, mapper_callable:); end
22
+
23
+ sig { returns(String) }
24
+ def archived_alias
25
+ "#{base_alias_name}.archive"
26
+ end
27
+
28
+ sig { returns(String) }
29
+ def active_alias
30
+ base_alias_name
31
+ end
32
+
33
+ sig { returns(String) }
34
+ def new_alias
35
+ "#{base_alias_name}.new"
36
+ end
37
+
38
+ sig { abstract.returns(String) }
39
+ def read_alias_name; end
40
+
41
+ sig { abstract.params(record: PostAddressData).returns(T::Hash[Symbol, T.untyped]) }
42
+ def record_source(record); end
43
+
44
+ sig { abstract.returns(String) }
45
+ def base_alias_name; end
46
+
47
+ sig do
48
+ abstract.params(
49
+ ensure_clean: T::Boolean,
50
+ raise_errors: T::Boolean,
51
+ ).void
52
+ end
53
+ def create_next_index(ensure_clean: false, raise_errors: false); end
54
+
55
+ sig do
56
+ abstract.params(
57
+ raise_errors: T::Boolean,
58
+ ).void
59
+ end
60
+ def switch_to_next_index(raise_errors: false); end
61
+
62
+ sig do
63
+ abstract.params(
64
+ records: T.any(ActiveRecord::Relation, T::Array[PostAddressData]),
65
+ ).returns(T.nilable(Response))
66
+ end
67
+ def save_records_backfill(records); end
68
+
69
+ sig { abstract.params(query: T::Hash[String, T.untyped]).returns(T::Hash[String, T.untyped]) }
70
+ def search(query); end
71
+
72
+ sig { abstract.params(id: T.any(String, Integer)).returns(T::Hash[String, T.untyped]) }
73
+ def find(id); end
74
+
75
+ sig { abstract.params(query: T::Hash[String, T.untyped]).returns(T::Hash[String, T.untyped]) }
76
+ def analyze(query); end
77
+
78
+ sig { abstract.params(query: T::Hash[String, T.untyped]).returns(T::Hash[String, T.untyped]) }
79
+ def term_vectors(query); end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,20 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module AtlasEngine
5
+ module Elasticsearch
6
+ class Response
7
+ extend T::Sig
8
+
9
+ sig { returns(T.untyped) }
10
+ attr_reader :response
11
+
12
+ delegate :body, :status, :headers, to: :response
13
+
14
+ sig { params(response: T.untyped).void }
15
+ def initialize(response)
16
+ @response = response
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,12 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module AtlasEngine
5
+ class Event < ApplicationRecord
6
+ include ActiveModel::Validations
7
+
8
+ belongs_to :country_import, class_name: "AtlasEngine::CountryImport"
9
+ validates :message, presence: true
10
+ enum category: { progress: 0, error: 1, invalid_address: 2 }
11
+ end
12
+ end
@@ -0,0 +1,36 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module AtlasEngine
5
+ class FieldDecompounder
6
+ extend T::Sig
7
+
8
+ attr_reader :field, :value, :country_profile
9
+
10
+ sig { params(field: Symbol, value: T.nilable(String), country_profile: CountryProfile).void }
11
+ def initialize(field:, value:, country_profile:)
12
+ @field = field
13
+ @value = value
14
+ @country_profile = country_profile
15
+ end
16
+
17
+ sig { returns(T.nilable(String)) }
18
+ def call
19
+ country_profile.decompounding_patterns(field)&.each do |pattern|
20
+ transliterated_street = ActiveSupport::Inflector.transliterate(value.to_s)
21
+ if (match = transliterated_street.match(expanded_pattern(pattern)))
22
+ return (match[:pre] + match[:name] + " " + match[:suffix] + match[:post]).strip
23
+ end
24
+ end
25
+
26
+ value
27
+ end
28
+
29
+ private
30
+
31
+ sig { params(pattern: String).returns(Regexp) }
32
+ def expanded_pattern(pattern)
33
+ /(?<pre>.*\b)#{pattern}(?<post>.*)/i
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,188 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module AtlasEngine
5
+ class IndexConfigurationFactory
6
+ extend T::Sig
7
+
8
+ IndexConfigurations = T.type_alias { T::Hash[T.any(String, Symbol), T.untyped] }
9
+
10
+ INDEX_CONFIGURATIONS_ROOT = T.let(
11
+ File.join(AtlasEngine::Engine.root, "db/data/address_synonyms/index_configurations"), String
12
+ )
13
+ COUNTRIES_ROOT = T.let(File.join(AtlasEngine::Engine.root, "app/countries/atlas_engine"), String)
14
+
15
+ DEFAULT_NUMBER_OF_SHARDS = 1
16
+ DEFAULT_NUMBER_OF_REPLICAS = 1
17
+ DEFAULT_MIN_ZIP_EDGE_GRAM = 1
18
+ DEFAULT_MAX_ZIP_EDGE_GRAM = 10
19
+
20
+ sig { returns(Symbol) }
21
+ attr_reader :country_code
22
+
23
+ sig { returns(T.nilable(String)) }
24
+ attr_reader :locale
25
+
26
+ sig { returns(CountryProfile) }
27
+ attr_reader :country_profile
28
+
29
+ sig { returns(T.nilable(Integer)) }
30
+ attr_reader :shard_override
31
+
32
+ sig { returns(T.nilable(Integer)) }
33
+ attr_reader :replica_override
34
+
35
+ sig do
36
+ params(
37
+ country_code: String,
38
+ locale: T.nilable(String),
39
+ shard_override: T.nilable(Integer),
40
+ replica_override: T.nilable(Integer),
41
+ ).void
42
+ end
43
+ def initialize(
44
+ country_code:,
45
+ locale: nil,
46
+ shard_override: nil,
47
+ replica_override: nil
48
+ )
49
+ @country_code = T.let(country_code.downcase.to_sym, Symbol)
50
+ @locale = T.let(locale.present? ? locale.downcase : nil, T.nilable(String))
51
+ @country = T.let(Worldwide.region(code: @country_code), Worldwide::Region)
52
+ @country_profile = T.let(CountryProfile.for(@country_code.to_s.upcase, @locale), CountryProfile)
53
+ @shard_override = T.let(shard_override, T.nilable(Integer))
54
+ @replica_override = T.let(replica_override, T.nilable(Integer))
55
+ end
56
+
57
+ sig do
58
+ params(
59
+ creating: T::Boolean,
60
+ ).returns(IndexConfigurations)
61
+ end
62
+ def index_configuration(creating: false)
63
+ remove_create_only_settings unless creating
64
+ configuration
65
+ end
66
+
67
+ private
68
+
69
+ sig { returns(IndexConfigurations) }
70
+ def configuration
71
+ @configuration ||= T.let(
72
+ defaults.deep_merge(country_overrides).deep_merge(locale_overrides),
73
+ T.nilable(IndexConfigurations),
74
+ )
75
+ end
76
+
77
+ sig { returns(IndexConfigurations) }
78
+ def defaults
79
+ render_file(:default)
80
+ end
81
+
82
+ sig { returns(IndexConfigurations) }
83
+ def country_overrides
84
+ render_file(country_code)
85
+ end
86
+
87
+ sig { returns(IndexConfigurations) }
88
+ def locale_overrides
89
+ return {} if locale.blank?
90
+
91
+ render_file(country_code, locale)
92
+ end
93
+
94
+ sig { params(country_code: Symbol, locale: T.nilable(String)).returns(IndexConfigurations) }
95
+ def render_file(country_code, locale = nil)
96
+ file_path = file_path(country_code, locale)
97
+
98
+ return {} if file_path.nil?
99
+
100
+ configuration_file = ActiveSupport::ConfigurationFile.new(file_path)
101
+ configuration_file.parse(context: binding).deep_stringify_keys
102
+ end
103
+
104
+ sig { params(country_code: Symbol, locale: T.nilable(String)).returns(T.nilable(String)) }
105
+ def file_path(country_code, locale = nil)
106
+ if country_code == :default
107
+ return File.join(INDEX_CONFIGURATIONS_ROOT, "default.yml")
108
+ end
109
+
110
+ path = if locale.present?
111
+ File.join(COUNTRIES_ROOT, "#{country_code}/locales/#{locale}/index_configuration.yml")
112
+ else
113
+ File.join(COUNTRIES_ROOT, "#{country_code}/index_configuration.yml")
114
+ end
115
+
116
+ path if File.file?(path)
117
+ end
118
+
119
+ sig { void }
120
+ def remove_create_only_settings
121
+ configuration.dig("settings", "index")&.delete("number_of_shards")
122
+ configuration.dig("settings", "index")&.delete("number_of_replicas")
123
+ end
124
+
125
+ sig { returns(T::Array[String]) }
126
+ def city_synonyms
127
+ synonyms[:city_synonyms] || []
128
+ end
129
+
130
+ sig { returns(T::Array[String]) }
131
+ def street_synonyms
132
+ synonyms[:street_synonyms] || []
133
+ end
134
+
135
+ sig { returns(T::Hash[Symbol, T::Array[String]]) }
136
+ def synonyms
137
+ @synonyms ||= T.let(
138
+ begin
139
+ file_name = if locale.present?
140
+ "#{COUNTRIES_ROOT}/#{country_code}/locales/#{locale}/synonyms.yml"
141
+ else
142
+ "#{COUNTRIES_ROOT}/#{country_code}/synonyms.yml"
143
+ end
144
+
145
+ empty_synonyms_hash = { street_synonyms: [], city_synonyms: [] }
146
+
147
+ File.file?(file_name) ? YAML.load_file(file_name, freeze: true).deep_symbolize_keys : empty_synonyms_hash
148
+ end,
149
+ T.nilable(T::Hash[Symbol, T::Array[String]]),
150
+ )
151
+ end
152
+
153
+ sig { returns(String) }
154
+ def number_of_shards
155
+ (
156
+ shard_override ||
157
+ country_profile.ingestion.settings_number_of_shards ||
158
+ DEFAULT_NUMBER_OF_SHARDS
159
+ ).to_s
160
+ end
161
+
162
+ sig { returns(String) }
163
+ def number_of_replicas
164
+ (
165
+ replica_override ||
166
+ country_profile.ingestion.settings_number_of_replicas ||
167
+ DEFAULT_NUMBER_OF_REPLICAS
168
+ ).to_s
169
+ end
170
+
171
+ sig { returns(String) }
172
+ def zip_edge_min_gram
173
+ country_profile.ingestion.settings_min_zip_edge_ngram || DEFAULT_MIN_ZIP_EDGE_GRAM.to_s
174
+ end
175
+
176
+ sig { returns(T.nilable(String)) }
177
+ def zip_edge_max_gram
178
+ country_profile.ingestion.settings_max_zip_edge_ngram ||
179
+ @country.zip_example&.length&.to_s ||
180
+ DEFAULT_MAX_ZIP_EDGE_GRAM.to_s
181
+ end
182
+
183
+ sig { returns(T::Boolean) }
184
+ def country_has_zip?
185
+ @country.has_zip?
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,114 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ module AtlasEngine
5
+ class PostAddress < ApplicationRecord
6
+ extend T::Sig
7
+
8
+ validates :country_code, :city, presence: true
9
+ validate :country_must_be_supported, :province_must_match_country, :zip_must_match_country_and_province
10
+ serialize :city, type: Array, coder: YAML
11
+
12
+ sig { returns(T::Hash[Symbol, T.untyped]) }
13
+ def to_h
14
+ {
15
+ source_id: source_id,
16
+ locale: locale,
17
+ country_code: country_code,
18
+ province_code: province_code,
19
+ region1: region1,
20
+ region2: region2,
21
+ region3: region3,
22
+ region4: region4,
23
+ city: city,
24
+ suburb: suburb,
25
+ zip: zip,
26
+ street: street,
27
+ building_name: building_name,
28
+ latitude: latitude,
29
+ longitude: longitude,
30
+ }.compact
31
+ end
32
+
33
+ private
34
+
35
+ sig { void }
36
+ def country_must_be_supported
37
+ if country_code.blank?
38
+ errors.add(:country, "is required")
39
+ elsif !country.country?
40
+ errors.add(:country, "with code '#{country_code}' is not recognized") unless country.country?
41
+ end
42
+ end
43
+
44
+ sig { void }
45
+ def province_must_match_country
46
+ return if country_code.blank?
47
+ return unless country_has_provinces?
48
+
49
+ if province_missing?
50
+ errors.add(:province_code, "is required for country '#{country_code}'") if province_missing?
51
+ elsif province_invalid?
52
+ errors.add(:province_code, "'#{province_code}' is invalid for country '#{country_code}'")
53
+ end
54
+ end
55
+
56
+ sig { void }
57
+ def zip_must_match_country_and_province
58
+ return if country_code.blank?
59
+
60
+ zip_must_match_country
61
+ return if errors[:zip].any?
62
+
63
+ zip_must_match_province
64
+ end
65
+
66
+ sig { void }
67
+ def zip_must_match_country
68
+ return unless country_has_zip_codes?
69
+
70
+ if zip.blank? && country.zip_required?
71
+ errors.add(:zip, "is required for country '#{country_code}'")
72
+ elsif !country.valid_zip?(zip)
73
+ errors.add(:zip, "'#{zip}' is invalid for country '#{country_code}'")
74
+ end
75
+ end
76
+
77
+ sig { void }
78
+ def zip_must_match_province
79
+ return unless province_code
80
+
81
+ province = country.zone(code: province_code)
82
+
83
+ return unless province.province?
84
+ return if province.valid_zip?(zip)
85
+
86
+ errors.add(:zip, "'#{zip}' is invalid for province '#{province_code}'")
87
+ end
88
+
89
+ sig { returns(Worldwide::Region) }
90
+ def country
91
+ @country ||= T.let(Worldwide.region(code: country_code), T.nilable(Worldwide::Region))
92
+ end
93
+
94
+ sig { returns(T::Boolean) }
95
+ def country_has_provinces?
96
+ country.zones.present? && !country.hide_provinces_from_addresses
97
+ end
98
+
99
+ sig { returns(T::Boolean) }
100
+ def country_has_zip_codes?
101
+ country.country? && country.has_zip?
102
+ end
103
+
104
+ sig { returns(T::Boolean) }
105
+ def province_invalid?
106
+ province_code.present? && !country.zone(code: province_code).province?
107
+ end
108
+
109
+ sig { returns(T::Boolean) }
110
+ def province_missing?
111
+ province_code.blank? && !country.province_optional?
112
+ end
113
+ end
114
+ end