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,88 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module AtlasEngine
5
+ module AddressValidation
6
+ module Validators
7
+ module FullAddress
8
+ class ComponentsToValidate
9
+ extend T::Sig
10
+
11
+ attr_reader :session, :candidate, :street_comparison
12
+
13
+ ALL_SUPPORTED_COMPONENTS = [
14
+ :province_code,
15
+ :city,
16
+ :zip,
17
+ :street,
18
+ ].freeze
19
+
20
+ sig do
21
+ params(
22
+ session: Session,
23
+ candidate: Candidate,
24
+ street_comparison: T.nilable(AtlasEngine::AddressValidation::Token::Sequence::Comparison),
25
+ ).void
26
+ end
27
+ def initialize(session, candidate, street_comparison)
28
+ @session = session
29
+ @candidate = candidate
30
+ @street_comparison = street_comparison
31
+ end
32
+
33
+ sig { returns(T::Array[Symbol]) }
34
+ def run
35
+ supported_components = ALL_SUPPORTED_COMPONENTS.dup - unsupported_components_for_country
36
+ supported_components.delete(:street) if exclude_street_validation?
37
+ supported_components
38
+ end
39
+
40
+ private
41
+
42
+ sig { returns(T::Boolean) }
43
+ def exclude_street_validation?
44
+ return true unless session.matching_strategy == AddressValidation::MatchingStrategies::EsStreet
45
+
46
+ if street_comparison.blank?
47
+ emit_excluded_validation("street", "not_found")
48
+ return true
49
+ end
50
+
51
+ if exclusions("street").any? { |exclusion| exclusion.apply?(session, candidate) }
52
+ emit_excluded_validation("street", "excluded")
53
+ return true
54
+ end
55
+
56
+ false
57
+ end
58
+
59
+ sig { params(component: String).returns(T::Array[T.class_of(Exclusions::ExclusionBase)]) }
60
+ def exclusions(component)
61
+ CountryProfile.for(session.country_code).validation.validation_exclusions(component: component)
62
+ end
63
+
64
+ sig { params(component: String, reason: String).void }
65
+ def emit_excluded_validation(component, reason)
66
+ tags = [
67
+ "reason:#{reason}",
68
+ "component:#{component}",
69
+ "country:#{session.country_code}",
70
+ ]
71
+ StatsD.increment("AddressValidation.skip", sample_rate: 1.0, tags: tags)
72
+ end
73
+
74
+ sig { returns(T::Array[Symbol]) }
75
+ def unsupported_components_for_country
76
+ unsupported_components = []
77
+ country = Worldwide.region(code: session.address.country_code)
78
+ unsupported_components << :province_code if country.province_optional?
79
+ unsupported_components << :province_code if country.hide_provinces_from_addresses
80
+ unsupported_components << :city unless country.city_required?
81
+ unsupported_components << :zip unless country.zip_required? && !country.zip_autofill_enabled
82
+ unsupported_components.uniq
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,127 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module AtlasEngine
5
+ module AddressValidation
6
+ module Validators
7
+ module FullAddress
8
+ class ConcernBuilder
9
+ extend T::Sig
10
+
11
+ attr_reader :unmatched_component, :unmatched_field, :matched_components, :address, :suggestion_ids
12
+
13
+ UNMATCHED_COMPONENTS_SUGGESTION_THRESHOLD = 2
14
+
15
+ class << self
16
+ extend T::Sig
17
+
18
+ sig do
19
+ params(
20
+ address: AbstractAddress,
21
+ unmatched_component_keys: T::Array[Symbol],
22
+ ).returns(T::Boolean)
23
+ end
24
+ def should_suggest?(address, unmatched_component_keys)
25
+ return false if too_many_unmatched_components?(unmatched_component_keys)
26
+
27
+ return false if province_and_city_xor_zip?(unmatched_component_keys) && !valid_zip_for_province?(address)
28
+
29
+ true
30
+ end
31
+
32
+ sig { params(unmatched_component_keys: T::Array[Symbol]).returns(T::Boolean) }
33
+ def too_many_unmatched_components?(unmatched_component_keys)
34
+ unmatched_component_keys.size > UNMATCHED_COMPONENTS_SUGGESTION_THRESHOLD
35
+ end
36
+
37
+ sig { params(address: AbstractAddress).returns(T::Boolean) }
38
+ def valid_zip_for_province?(address)
39
+ !country_has_zip_codes(address) || province_postal_code_valid?(address)
40
+ end
41
+
42
+ private
43
+
44
+ sig { params(component_keys: T::Array[Symbol]).returns(T::Boolean) }
45
+ def province_and_city_xor_zip?(component_keys)
46
+ component_keys.include?(:province_code) && component_keys.intersection([:zip, :city]).one?
47
+ end
48
+
49
+ sig { params(address: AbstractAddress).returns(T::Boolean) }
50
+ def country_has_zip_codes(address)
51
+ Worldwide.region(code: address.country_code).has_zip?
52
+ end
53
+
54
+ def province_postal_code_valid?(address)
55
+ return true if address.province_code.blank?
56
+
57
+ country = Worldwide.region(code: address.country_code)
58
+ return true if country.hide_provinces_from_addresses
59
+
60
+ province = country.zone(code: address.province_code)
61
+ return true unless province.province?
62
+
63
+ province.valid_zip?(address.zip)
64
+ end
65
+ end
66
+
67
+ sig do
68
+ params(
69
+ unmatched_component: Symbol,
70
+ matched_components: T::Array[Symbol],
71
+ address: AbstractAddress,
72
+ suggestion_ids: T::Array[String],
73
+ unmatched_field: T.nilable(Symbol),
74
+ ).void
75
+ end
76
+ def initialize(unmatched_component:, matched_components:, address:, suggestion_ids:, unmatched_field: nil)
77
+ @unmatched_component = unmatched_component
78
+ @unmatched_field = unmatched_field
79
+ @matched_components = matched_components
80
+ @address = address
81
+ @suggestion_ids = suggestion_ids
82
+ end
83
+
84
+ sig { returns(AddressValidation::Concern) }
85
+ def build
86
+ case unmatched_component
87
+ when :zip
88
+ build_zip_concern
89
+ when :province_code
90
+ build_province_concern
91
+ else
92
+ build_default_concern
93
+ end
94
+ end
95
+
96
+ private
97
+
98
+ sig { returns(AddressValidation::Concern) }
99
+ def build_zip_concern
100
+ concern = InvalidZipConcernBuilder.for(address, suggestion_ids)
101
+ return concern if concern
102
+
103
+ if :province_code.in?(matched_components) && :city.in?(matched_components)
104
+ return UnknownZipForAddressConcern.new(address, suggestion_ids)
105
+ end
106
+
107
+ build_default_concern
108
+ end
109
+
110
+ sig { returns(AddressValidation::Concern) }
111
+ def build_province_concern
112
+ if ([:zip, :city] - matched_components).empty?
113
+ UnknownProvinceConcern.new(address, suggestion_ids)
114
+ else
115
+ build_default_concern
116
+ end
117
+ end
118
+
119
+ sig { returns(AddressValidation::Concern) }
120
+ def build_default_concern
121
+ UnmatchedFieldConcern.new(unmatched_component, matched_components, address, suggestion_ids, unmatched_field)
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,23 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module AtlasEngine
5
+ module AddressValidation
6
+ module Validators
7
+ module FullAddress
8
+ module Exclusions
9
+ class ExclusionBase
10
+ class << self
11
+ extend T::Sig
12
+ extend T::Helpers
13
+ abstract!
14
+
15
+ sig { abstract.params(session: Session, candidate: Candidate).returns(T::Boolean) }
16
+ def apply?(session, candidate); end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,42 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module AtlasEngine
5
+ module AddressValidation
6
+ module Validators
7
+ module FullAddress
8
+ class InvalidZipConcernBuilder
9
+ class << self
10
+ extend T::Sig
11
+
12
+ sig do
13
+ params(
14
+ address: AbstractAddress,
15
+ suggestion_ids: T::Array[String],
16
+ ).returns(T.nilable(AddressValidation::Concern))
17
+ end
18
+ def for(address, suggestion_ids)
19
+ country = Worldwide.region(code: address.country_code)
20
+
21
+ province = country.zone(code: address.province_code.presence || "")
22
+ return unless country.has_zip?
23
+
24
+ if country_expects_zone_in_address?(country) && province.province?
25
+ InvalidZipForProvinceConcern.new(address, suggestion_ids) unless province.valid_zip?(address.zip)
26
+ else
27
+ InvalidZipForCountryConcern.new(address, suggestion_ids) unless country.valid_zip?(address.zip)
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ sig { params(country: Worldwide::Region).returns(T::Boolean) }
34
+ def country_expects_zone_in_address?(country)
35
+ country.zones&.any?(&:province?) && !country.hide_provinces_from_addresses
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,37 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module AtlasEngine
5
+ module AddressValidation
6
+ module Validators
7
+ module FullAddress
8
+ class InvalidZipForCountryConcern < AddressValidation::Concern
9
+ include ConcernFormatter
10
+ attr_reader :address
11
+
12
+ sig { params(address: AbstractAddress, suggestion_ids: T::Array[String]).void }
13
+ def initialize(address, suggestion_ids)
14
+ @address = address
15
+
16
+ super(
17
+ code: :zip_invalid_for_country,
18
+ field_names: [:zip],
19
+ message: message,
20
+ type: T.must(Concern::TYPES[:error]),
21
+ type_level: 1,
22
+ suggestion_ids: suggestion_ids
23
+ )
24
+ end
25
+
26
+ sig { returns(String) }
27
+ def message
28
+ country.field(key: :zip).error(
29
+ code: :invalid_for_country,
30
+ options: { country: country.full_name },
31
+ ).to_s
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,37 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module AtlasEngine
5
+ module AddressValidation
6
+ module Validators
7
+ module FullAddress
8
+ class InvalidZipForProvinceConcern < AddressValidation::Concern
9
+ include ConcernFormatter
10
+ attr_reader :address
11
+
12
+ sig { params(address: AbstractAddress, suggestion_ids: T::Array[String]).void }
13
+ def initialize(address, suggestion_ids)
14
+ @address = address
15
+
16
+ super(
17
+ code: :zip_invalid_for_province,
18
+ field_names: [:zip],
19
+ message: message,
20
+ type: T.must(Concern::TYPES[:error]),
21
+ type_level: 1,
22
+ suggestion_ids: suggestion_ids
23
+ )
24
+ end
25
+
26
+ sig { returns(String) }
27
+ def message
28
+ country.field(key: :zip).error(
29
+ code: :invalid_for_province,
30
+ options: { province: province_name },
31
+ ).to_s
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,26 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module AtlasEngine
5
+ module AddressValidation
6
+ module Validators
7
+ module FullAddress
8
+ class NoCandidateResult < CandidateResultBase
9
+ extend T::Sig
10
+
11
+ sig { void }
12
+ def update_result
13
+ result.concerns << UnknownAddressConcern.new(
14
+ address,
15
+ )
16
+
17
+ concern = InvalidZipConcernBuilder.for(session.address, [])
18
+ result.concerns << concern if concern
19
+
20
+ update_result_scope
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,31 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module AtlasEngine
5
+ module AddressValidation
6
+ module Validators
7
+ module FullAddress
8
+ class NumberComparison
9
+ include Comparable
10
+
11
+ attr_reader :numbers, :candidate_ranges
12
+
13
+ def initialize(numbers: [], candidate_ranges: [])
14
+ @numbers = numbers
15
+ @candidate_ranges = candidate_ranges
16
+ end
17
+
18
+ def match?
19
+ return true if candidate_ranges.blank? && numbers.present?
20
+
21
+ numbers.compact.any? do |number|
22
+ candidate_ranges.any? do |candidate_range|
23
+ candidate_range.include?(number)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,60 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ # This class is responsible for checking whether the session postal code is a valid partial length.
5
+ # If valid, #truncate will return a truncated candidate postal code that corresponds to a valid partial range.
6
+ # Else, #truncate will return the unmodified candidate postal code.
7
+
8
+ module AtlasEngine
9
+ module AddressValidation
10
+ module Validators
11
+ module FullAddress
12
+ class PostalCodeMatcher
13
+ extend T::Sig
14
+ include LogHelper
15
+
16
+ attr_reader :country_code, :session_postal_code, :candidate_postal_code
17
+
18
+ sig do
19
+ params(country_code: String, session_postal_code: String, candidate_postal_code: T.nilable(String)).void
20
+ end
21
+ def initialize(country_code, session_postal_code, candidate_postal_code = nil)
22
+ @country_code = country_code
23
+ @session_postal_code = session_postal_code
24
+ @candidate_postal_code = candidate_postal_code
25
+ end
26
+
27
+ sig { returns(T.nilable(String)) }
28
+ def truncate
29
+ return unless candidate_postal_code
30
+ return candidate_postal_code if candidate_postal_code.size <= session_postal_code.size
31
+ return candidate_postal_code unless valid_partial_postal_code_length?
32
+
33
+ truncated_postal_code = candidate_postal_code[partial_postal_code_range]
34
+
35
+ log_info("Truncating candidate postal code", {
36
+ session_postal_code: session_postal_code,
37
+ original_postal_code: candidate_postal_code,
38
+ truncated_postal_code: truncated_postal_code,
39
+ })
40
+
41
+ truncated_postal_code
42
+ end
43
+
44
+ private
45
+
46
+ sig { returns(T::Boolean) }
47
+ def valid_partial_postal_code_length?
48
+ partial_postal_code_range.present?
49
+ end
50
+
51
+ sig { returns(T.nilable(Range)) }
52
+ def partial_postal_code_range
53
+ @partial_postal_code_range ||= CountryProfile.for(country_code)
54
+ .validation.partial_postal_code_range(session_postal_code.size)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,42 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module AtlasEngine
5
+ module AddressValidation
6
+ module Validators
7
+ module FullAddress
8
+ class ResultUpdater
9
+ extend T::Sig
10
+ extend T::Helpers
11
+ abstract!
12
+
13
+ sig { params(session: Session, result: Result).void }
14
+ def initialize(session:, result:)
15
+ @session = session
16
+ @result = result
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :session, :result
22
+
23
+ delegate :address, to: :session
24
+
25
+ sig { void }
26
+ def update_result_scope
27
+ concern_fields = result.concerns.flat_map(&:field_names).uniq
28
+ scopes_to_remove = concern_fields.flat_map { |field| contained_scopes_for(field) }
29
+ result.validation_scope.reject! { |scope| scope.in?(scopes_to_remove) }
30
+ end
31
+
32
+ sig { params(scope: Symbol).returns(T.nilable(T::Array[Symbol])) }
33
+ def contained_scopes_for(scope)
34
+ return [] unless (scope_index = Result::SORTED_VALIDATION_SCOPES.index(scope))
35
+
36
+ Result::SORTED_VALIDATION_SCOPES.slice(scope_index..)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,140 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module AtlasEngine
5
+ module AddressValidation
6
+ module Validators
7
+ module FullAddress
8
+ class SuggestionBuilder
9
+ class << self
10
+ extend T::Sig
11
+ sig do
12
+ params(
13
+ address: {
14
+ address1: T.nilable(String),
15
+ address2: T.nilable(String),
16
+ city: T.nilable(String),
17
+ province_code: T.nilable(String),
18
+ country_code: T.nilable(String),
19
+ zip: T.nilable(String),
20
+ phone: T.nilable(String),
21
+ },
22
+ comparisons: T::Hash[Symbol, AtlasEngine::AddressValidation::Token::Sequence::Comparison],
23
+ candidate: AddressValidation::Candidate,
24
+ unmatched_fields: T::Hash[Symbol, Symbol],
25
+ ).returns(Suggestion)
26
+ end
27
+ def from_comparisons(address, comparisons, candidate, unmatched_fields = {})
28
+ unmatched_address_keys = comparisons.keys.each_with_object([]) do |key, array|
29
+ array << if key == :street
30
+ unmatched_fields[:street]
31
+ else
32
+ key
33
+ end
34
+ end.append(:country_code).compact
35
+
36
+ suggestion = Suggestion.new(**T.unsafe(address).slice(*unmatched_address_keys))
37
+
38
+ comparisons.each do |key, comparison|
39
+ # suggestion.send("suggest_#{key}", comparison, candidate, unmatched_fields)
40
+ case key
41
+ when :street
42
+ suggest_street(suggestion, comparison, candidate, unmatched_fields)
43
+ when :city
44
+ suggest_city(suggestion, comparison, candidate, unmatched_fields)
45
+ when :zip
46
+ suggest_zip(suggestion, comparison, candidate, unmatched_fields)
47
+ when :province_code
48
+ suggest_province_code(suggestion, comparison, candidate, unmatched_fields)
49
+ end
50
+ end
51
+
52
+ # Since the suggestion does not suggest the new country, we can safely remove it.
53
+ suggestion.country_code = nil
54
+
55
+ suggestion
56
+ end
57
+
58
+ private
59
+
60
+ sig do
61
+ params(
62
+ suggestion: Suggestion,
63
+ comparison: AtlasEngine::AddressValidation::Token::Sequence::Comparison,
64
+ candidate: AddressValidation::Candidate,
65
+ unmatched_fields: T::Hash[Symbol, Symbol],
66
+ ).returns(Suggestion)
67
+ end
68
+ def suggest_street(suggestion, comparison, candidate, unmatched_fields)
69
+ suggested_street = comparison.right_sequence.raw_value
70
+ original_street = comparison.left_sequence.raw_value
71
+ field = unmatched_fields[:street]
72
+
73
+ if field == :address1
74
+ suggestion.address1 = suggestion.address1.to_s.sub(original_street, suggested_street)
75
+ elsif field == :address2
76
+ suggestion.address2 = suggestion.address2.to_s.sub(original_street, suggested_street)
77
+ end
78
+
79
+ suggestion
80
+ end
81
+
82
+ sig do
83
+ params(
84
+ suggestion: Suggestion,
85
+ comparison: AtlasEngine::AddressValidation::Token::Sequence::Comparison,
86
+ candidate: AddressValidation::Candidate,
87
+ _unmatched_fields: T::Hash[Symbol, Symbol],
88
+ ).returns(Suggestion)
89
+ end
90
+ def suggest_city(suggestion, comparison, candidate, _unmatched_fields)
91
+ suggestion.city = generic_field_suggestion(comparison, candidate, :city)
92
+ suggestion
93
+ end
94
+
95
+ sig do
96
+ params(
97
+ suggestion: Suggestion,
98
+ comparison: AtlasEngine::AddressValidation::Token::Sequence::Comparison,
99
+ candidate: AddressValidation::Candidate,
100
+ _unmatched_fields: T::Hash[Symbol, Symbol],
101
+ ).returns(Suggestion)
102
+ end
103
+ def suggest_zip(suggestion, comparison, candidate, _unmatched_fields)
104
+ suggestion.zip = generic_field_suggestion(comparison, candidate, :zip)
105
+ suggestion
106
+ end
107
+
108
+ sig do
109
+ params(
110
+ suggestion: Suggestion,
111
+ comparison: AtlasEngine::AddressValidation::Token::Sequence::Comparison,
112
+ candidate: AddressValidation::Candidate,
113
+ _unmatched_fields: T::Hash[Symbol, Symbol],
114
+ ).returns(Suggestion)
115
+ end
116
+ def suggest_province_code(suggestion, comparison, candidate, _unmatched_fields)
117
+ suggestion.province_code = generic_field_suggestion(comparison, candidate, :province_code)
118
+ suggestion
119
+ end
120
+
121
+ sig do
122
+ params(
123
+ comparison: AtlasEngine::AddressValidation::Token::Sequence::Comparison,
124
+ candidate: AddressValidation::Candidate,
125
+ field: Symbol,
126
+ ).returns(String)
127
+ end
128
+ def generic_field_suggestion(comparison, candidate, field)
129
+ if comparison.token_match_count == 0 || comparison.aggregate_edit_distance > 2
130
+ candidate.component(field)&.first_value
131
+ else
132
+ comparison.right_sequence.raw_value
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,30 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module AtlasEngine
5
+ module AddressValidation
6
+ module Validators
7
+ module FullAddress
8
+ class UnknownAddressConcern < AddressValidation::Concern
9
+ include ConcernFormatter
10
+
11
+ sig { returns(TAddress) }
12
+ attr_reader :address
13
+
14
+ sig { params(address: TAddress).void }
15
+ def initialize(address)
16
+ @address = address
17
+ super(
18
+ code: :address_unknown,
19
+ message: Worldwide.region(code: address.country_code).field(key: :address).error(code: :may_not_exist),
20
+ type: T.must(Concern::TYPES[:warning]),
21
+ type_level: 1,
22
+ suggestion_ids: [],
23
+ field_names: [:address1],
24
+ )
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end