atlas_engine 0.8.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +57 -13
  3. data/app/countries/atlas_engine/at/country_profile.yml +1 -1
  4. data/app/countries/atlas_engine/au/country_profile.yml +1 -1
  5. data/app/countries/atlas_engine/be/country_profile.yml +2 -2
  6. data/app/countries/atlas_engine/ch/country_profile.yml +1 -1
  7. data/app/countries/atlas_engine/fr/country_profile.yml +1 -1
  8. data/app/countries/atlas_engine/gb/address_validation/es/query_builder.rb +1 -1
  9. data/app/countries/atlas_engine/nl/country_profile.yml +1 -1
  10. data/app/countries/atlas_engine/pl/country_profile.yml +1 -1
  11. data/app/countries/atlas_engine/si/address_importer/open_address/mapper.rb +1 -1
  12. data/app/countries/atlas_engine/us/address_importer/open_address/filter.rb +28 -0
  13. data/app/countries/atlas_engine/us/address_importer/open_address/mapper.rb +66 -0
  14. data/app/countries/atlas_engine/us/country_profile.yml +9 -4
  15. data/app/countries/atlas_engine/us/jobs/address_importer/combined_import_job.rb +120 -0
  16. data/app/graphql/atlas_engine/schema.graphql +4 -0
  17. data/app/graphql/atlas_engine/types/address_validation/address_input.rb +4 -0
  18. data/app/jobs/atlas_engine/address_importer/open_address/geo_json_import_job.rb +3 -2
  19. data/app/lib/atlas_engine/validation_transcriber/formatter.rb +10 -1
  20. data/app/models/atlas_engine/address_importer/open_address/default_mapper.rb +4 -3
  21. data/app/models/atlas_engine/address_importer/open_address/transformer.rb +3 -2
  22. data/app/models/atlas_engine/address_number_range.rb +1 -1
  23. data/app/models/atlas_engine/address_validation/abstract_address.rb +12 -0
  24. data/app/models/atlas_engine/address_validation/address.rb +8 -0
  25. data/app/models/atlas_engine/address_validation/concern_record.rb +28 -0
  26. data/app/models/atlas_engine/address_validation/validator.rb +2 -0
  27. data/app/models/atlas_engine/address_validation/validators/full_address/suggestion_builder.rb +4 -0
  28. data/app/models/atlas_engine/address_validation/validators/full_address/unmatched_field_concern_builder.rb +7 -14
  29. data/app/models/atlas_engine/address_validation/validators/predicates/city/present.rb +0 -1
  30. data/app/models/atlas_engine/address_validation/validators/predicates/no_emojis.rb +0 -2
  31. data/app/models/atlas_engine/address_validation/validators/predicates/no_html_tags.rb +0 -2
  32. data/app/models/atlas_engine/address_validation/validators/predicates/no_url.rb +0 -2
  33. data/app/models/atlas_engine/address_validation/validators/predicates/not_exceed_max_length.rb +0 -2
  34. data/app/models/atlas_engine/address_validation/validators/predicates/phone/valid.rb +0 -2
  35. data/app/models/atlas_engine/address_validation/validators/predicates/province/exists.rb +0 -1
  36. data/app/models/atlas_engine/address_validation/validators/predicates/province/valid_for_country.rb +0 -1
  37. data/app/models/atlas_engine/address_validation/validators/predicates/street/building_number_in_address1.rb +0 -2
  38. data/app/models/atlas_engine/address_validation/validators/predicates/street/building_number_in_address1_or_address2.rb +0 -1
  39. data/app/models/atlas_engine/address_validation/validators/predicates/street/present.rb +0 -2
  40. data/app/models/atlas_engine/address_validation/validators/predicates/zip/zip_base.rb +0 -1
  41. data/app/tasks/maintenance/atlas_engine/geo_json_import_task.rb +0 -3
  42. data/app/tasks/maintenance/atlas_engine/us_geo_json_directory_import_task.rb +20 -0
  43. data/lib/atlas_engine/version.rb +1 -1
  44. metadata +21 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1bdadba3857324dae37bf4c22aa4078b4f565876c0f865a4d3b99db69d0eaa41
4
- data.tar.gz: 4004ade11bd387145ed6b82cb8b5df9a99989b3d1869cc1d40fd7fb310e3fc7c
3
+ metadata.gz: dde673ac6063402f30381011574f018aee19ac9d6e9b961c04252cf76cfd49e5
4
+ data.tar.gz: b5c0adce91a39d744004b902e2b3a4a13544a50af756bb1ace003e391b0d3643
5
5
  SHA512:
6
- metadata.gz: 846730be55864274ddd3d61e90ffd970102ea54d25221e411cee78c91daaa974a1159d0c976cde708ac6ea355815deb9c1d91230ef9c4eb49dcf38fe59e4be27
7
- data.tar.gz: 791c4a0050adab0b39092b660c56968d7f6862a1387914f50341f940153963487f9fc8ec8533ee6519fd8569bd69022c8cb7ce181c3017a14313fd32a49879a7
6
+ metadata.gz: dfe8b1e2a4ebe506f55b62a541b57ab22c0cca456ed4ef071007d43e106d0ed0b904389fd22a0424661ca76cd2f7bdc4ed5887866663eaee361f9afd5e7d3ac0
7
+ data.tar.gz: a632f397142d60a6da29ae440ed686ff920cb15e62349c3abb0ef9a3ccaddd975936870d55aee45867851337f10d46982f6740bbcf3379f9d74aeeed15369577
data/README.md CHANGED
@@ -7,6 +7,7 @@ Atlas Engine is a rails engine that provides a global end-to-end address validat
7
7
  * [Local Development Installation](#local-development-installation)
8
8
  * [Address Data Ingestion](#address-data-ingestion)
9
9
  * [Elasticsearch Matching Strategy](#elasticsearch-matching-strategy)
10
+ * [Hosted Solution](#hosted-solution)
10
11
 
11
12
  ## Address Validation API
12
13
 
@@ -167,17 +168,23 @@ The validation scope excludes zip because the zip was not successfully validated
167
168
  ## Rails App Installation
168
169
 
169
170
  ### Initial setup
170
- Add the engine to your gemfile
171
+ * Add the engine to your gemfile
171
172
  ```
172
173
  gem "atlas_engine"
173
174
  ```
174
175
 
175
- Run the following commands to install the engine in your rails app
176
-
176
+ * Run the following commands to install the engine in your rails app
177
177
  ```
178
178
  bundle lock
179
- bin/rails generate atlas_engine:install
179
+ rails atlas_engine:install:migrations
180
+ rails db:migrate
180
181
  ```
182
+ * In `config/routes` mount AtlasEngine
183
+ * Adding the line `mount AtlasEngine::Engine => "/atlas_engine"
184
+ `
185
+ * In `app/assets/config/manifest.js`
186
+ * Adding the line `//= link atlas_engine/application.css`
187
+ * Install [maintenance_tasks](https://github.com/Shopify/maintenance_tasks?tab=readme-ov-file#installation) - a dependency for Atlas Engine that is used to ingest country data.
181
188
 
182
189
  ### Updating to a newer version of the engine
183
190
 
@@ -297,23 +304,25 @@ At the moment, `atlas_engine` supports advanced address validation for the follo
297
304
 
298
305
  | Country/territory | Two-letter code | Locales | Street | City | Postal Code | Province/State |
299
306
  |-------------------|-----------------|----------|--------|------|-------------|----------------|
300
- | Australia | AU | | | x | x | x |
301
- | Austria | AT | | | x | x | x |
302
- | Belgium | BE | fr,nl,de | | x | x | |
307
+ | Australia | AU | | x | x | x | x |
308
+ | Austria | AT | | x | x | x | x |
309
+ | Belgium | BE | fr,nl,de | x | x | x | |
303
310
  | Bermuda | BM | | | x | x | x |
304
311
  | Czechia | CZ | | | x | x | |
305
312
  | Denmark | DK | | | x | x | |
306
313
  | Faroe Islands | FO | | | x | x | |
307
- | France | FR | | | x | x | |
314
+ | France | FR | | x | x | x | |
315
+ | Gurnsey | GG | | | x | x | |
308
316
  | Italy | IT | | | | x | |
309
317
  | Liechtenstein | LI | | | x | x | x |
310
318
  | Luxembourg | LU | fr,lb | | x | x | |
311
- | Netherlands | NL | nl | | x | x | x |
312
- | Poland | PL | | | x | x | x |
319
+ | Netherlands | NL | nl | x | x | x | x |
320
+ | Poland | PL | | x | x | x | x |
313
321
  | Portugal | PT | | | x | x | x |
314
322
  | Slovenia | SI | | | x | x | x |
315
323
  | South Korea | KR | | | x | x | x |
316
- | Switzerland | CH | de,fr,it | | x | x | |
324
+ | Switzerland | CH | de,fr,it | x | x | x | |
325
+ | United States | US | en | x | x | x | x |
317
326
 
318
327
  ### Downloading and indexing instructions
319
328
 
@@ -353,8 +362,7 @@ In this example, the country code of Australia is `AU`.
353
362
  link for a more detailed view. Once the import status has updated from `in_progress` to `complete` we will have all of
354
363
  the raw open address data imported into our mysql database's `atlas_engine_post_addresses` table.
355
364
 
356
- 6. Navigate back to `http://localhost:3000/maintenance_tasks` and click on the `Maintenance::AtlasEngine::ElasticsearchIndexCreateTask`. This task will ingest the data we have staged in mysql
357
- and use it to create documents in a new elasticsearch index which Atlas Engine will ultimately use for validation.
365
+ 6. Navigate back to `http://localhost:3000/maintenance_tasks` and click on the `Maintenance::AtlasEngine::ElasticsearchIndexCreateTask`. This task will ingest the data we have staged in mysql and use it to create documents in a new elasticsearch index which Atlas Engine will ultimately use for validation.
358
366
 
359
367
  7. The `ElasticsearchIndexCreateTask` includes the following parameters:
360
368
 
@@ -380,6 +388,26 @@ If unchecked, the created index will need to be activated manually.
380
388
  We may now use the `es` and `es_street` matching strategies with `AU` addresses. See [below](#elasticsearch-matching-strategy)
381
389
  for an example of its usage.
382
390
 
391
+ #### Instructions for US import
392
+
393
+ 1. Go to the [open addresses](https://openaddresses.io/) download center and download the collection-us-{region}.zip
394
+ files for each of the four regions (west, midwest, northeast, south).
395
+
396
+ 2. Run the US create state geojson script to create a statewide geojson.gz file for each state
397
+ ```
398
+ bin/us_create_state_geojson execute /path/to/us_collection_zips /path/to/output_dir
399
+ ```
400
+
401
+ 3. Start your app with `rails s` and navigate to `http://localhost:3000/maintenance_tasks`. There is a task only used
402
+ for the US import called `Maintenance::AtlasEngine::UsGeoJsonDirectoryImportTask`
403
+
404
+ 4. Parameterize the `UsGeoJsonDirectoryImportTask` with the output directory that contains all of the `{state}-statewide.geojson.gz` files created in step 2.
405
+
406
+ 5. Once properly parameterized, click run. The process will initialize a `country_import` and should succeed immediately.
407
+
408
+ 6. Navigate to `http://localhost:3000/country_imports` to track the progress of the country import. Once the import is complete
409
+ and the US data is in mysql the rest of the process for creating the elasticsearch index and verifying should be the same as above.
410
+
383
411
  ## Elasticsearch Matching Strategy
384
412
 
385
413
  Once we have successfully created and activated an elasticsearch index using open address data, we may now use
@@ -627,3 +655,19 @@ will produce the response:
627
655
  ```
628
656
 
629
657
  This response has no concerns or suggestions, and the input address is therefore considered to be valid.
658
+
659
+ ## Hosted Solution
660
+
661
+ If you wish to use the Shopify hosted version of the `atlas_engine` codebase in your applications, you will need to [register for an api key](https://address-validation.shopify.dev/#request-api-key) and agree to our [terms and conditions](https://address-validation.shopify.dev/terms-of-service).
662
+
663
+ Once you have successfully redeemed an api key, you will be able to access the `/graphql` endpoint, which can be queried using this example curl request:
664
+
665
+ ```
666
+ curl --request POST \
667
+ --url https://atlas-validation.shopifyapps.com/graphql \
668
+ --header 'Authorization: Bearer {your-api-key}' \
669
+ --header 'Content-Type: application/json' \
670
+ --data '{"query":"query validation { validation(address: { address1: \"233 S Wacker Dr\" address2: \"\" city: \"Chicago\" countryCode: US provinceCode: \"IL\" zip: \"60606\" } locale: \"EN\" ) { validationScope concerns { code fieldNames suggestionIds type typeLevel message } suggestions { id address1 address2 city province provinceCode zip } }}","operationName":"validation"}'
671
+ ```
672
+
673
+ Any updates to the `atlas_engine` codebase that are merged into our `main` branch will be deployed to our hosted solution as well.
@@ -9,7 +9,7 @@ ingestion:
9
9
  data_mapper: AtlasEngine::AddressValidation::Es::DataMappers::DecompoundingDataMapper
10
10
  validation:
11
11
  enabled: true
12
- default_matching_strategy: es
12
+ default_matching_strategy: es_street
13
13
  has_provinces: true
14
14
  address_parser: AtlasEngine::At::ValidationTranscriber::AddressParser
15
15
  normalized_components:
@@ -8,6 +8,6 @@ ingestion:
8
8
  validation:
9
9
  address_parser: AtlasEngine::Au::ValidationTranscriber::AddressParser
10
10
  enabled: true
11
- default_matching_strategy: es
11
+ default_matching_strategy: es_street
12
12
  open_address:
13
13
  filter: AtlasEngine::Au::AddressImporter::OpenAddress::Filter
@@ -5,9 +5,9 @@ ingestion:
5
5
  max_zip_edge_ngram: "4"
6
6
  validation:
7
7
  enabled: true
8
- default_matching_strategy: local
8
+ default_matching_strategy: es_street
9
9
  address_parser: AtlasEngine::Be::ValidationTranscriber::AddressParser
10
- has_provinces: false
10
+ has_provinces: false
11
11
  index_locales:
12
12
  - fr
13
13
  - nl
@@ -1,7 +1,7 @@
1
1
  id: CH
2
2
  validation:
3
3
  enabled: true
4
- default_matching_strategy: local
4
+ default_matching_strategy: es_street
5
5
  has_provinces: false
6
6
  address_parser: AtlasEngine::De::ValidationTranscriber::AddressParser
7
7
  index_locales:
@@ -10,6 +10,6 @@ ingestion:
10
10
  validation:
11
11
  address_parser: AtlasEngine::Fr::ValidationTranscriber::AddressParser
12
12
  enabled: true
13
- default_matching_strategy: es
13
+ default_matching_strategy: es_street
14
14
  has_provinces: false
15
15
  zip_prefix_length: 3
@@ -16,7 +16,7 @@ module AtlasEngine
16
16
  ).void
17
17
  end
18
18
  def initialize(address, parsings, profile)
19
- super(address, parsings, profile)
19
+ super
20
20
 
21
21
  @parsings = T.let(
22
22
  AtlasEngine::Gb::ValidationTranscriber::FullAddressParser
@@ -7,7 +7,7 @@ ingestion:
7
7
  validation:
8
8
  enabled: true
9
9
  has_provinces: true
10
- default_matching_strategy: es
10
+ default_matching_strategy: es_street
11
11
  normalized_components:
12
12
  - street_decompounded
13
13
  address_parser: AtlasEngine::Nl::ValidationTranscriber::AddressParser
@@ -7,7 +7,7 @@ ingestion:
7
7
  validation:
8
8
  address_parser: AtlasEngine::Pl::ValidationTranscriber::AddressParser
9
9
  enabled: true
10
- default_matching_strategy: es
10
+ default_matching_strategy: es_street
11
11
  exclusions:
12
12
  city:
13
13
  - AtlasEngine::Pl::AddressValidation::Exclusions::RuralAddress
@@ -10,7 +10,7 @@ module AtlasEngine
10
10
  params(feature: AtlasEngine::AddressImporter::OpenAddress::Feature).returns(T::Hash[Symbol, T.untyped])
11
11
  end
12
12
  def map(feature)
13
- super(feature).merge(region4: feature["properties"]["district"])
13
+ super.merge(region4: feature["properties"]["district"])
14
14
  end
15
15
  end
16
16
  end
@@ -0,0 +1,28 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module AtlasEngine
5
+ module Us
6
+ module AddressImporter
7
+ module OpenAddress
8
+ class Filter
9
+ extend T::Sig
10
+ include AtlasEngine::AddressImporter::OpenAddress::Filter
11
+
12
+ def initialize(country_import:); end
13
+
14
+ sig { override.params(feature: AtlasEngine::AddressImporter::OpenAddress::Feature).returns(T::Boolean) }
15
+ def filter(feature)
16
+ # Only consider features with lat lon geometry
17
+ geometry = feature["geometry"]
18
+ if geometry.present? && geometry["type"] == "Point"
19
+ return false if geometry["coordinates"].size > 2
20
+ end
21
+
22
+ true
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,66 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module AtlasEngine
5
+ module Us
6
+ module AddressImporter
7
+ module OpenAddress
8
+ class Mapper < AtlasEngine::AddressImporter::OpenAddress::DefaultMapper
9
+ ORDINAL_REGEX = /\b(\d+)(?:\s+)(st|nd|rd|th)\b/i
10
+
11
+ sig do
12
+ params(feature: AtlasEngine::AddressImporter::OpenAddress::Feature).returns(T::Hash[Symbol, T.untyped])
13
+ end
14
+ def map(feature)
15
+ city, street, number, unit, postcode = feature["properties"].values_at(
16
+ "city",
17
+ "street",
18
+ "number",
19
+ "unit",
20
+ "postcode",
21
+ )
22
+ {
23
+ source_id: openaddress_source_id(feature),
24
+ locale: @locale,
25
+ country_code: @country_code,
26
+ province_code: @province_code, # region is inconsistently set, override with passed in province_code
27
+ # Omitted: region1..4
28
+ city: sanitize_city(city),
29
+ suburb: nil,
30
+ zip: postcode.first(5), # truncate zip+4 data
31
+ street: sanitize_street(street),
32
+ building_and_unit_ranges: housenumber_and_unit(number, unit),
33
+ latitude: geometry(feature)&.at(1),
34
+ longitude: geometry(feature)&.at(0),
35
+ }
36
+ end
37
+
38
+ private
39
+
40
+ sig { params(street: T.nilable(String)).returns(T.nilable(String)) }
41
+ def sanitize_street(street)
42
+ combine_ordinal_string(strip_extra_spaces(street&.downcase))&.titleize
43
+ end
44
+
45
+ sig { params(city: T.nilable(String)).returns(T::Array[String]) }
46
+ def sanitize_city(city)
47
+ return [] if city.nil?
48
+
49
+ city.split("/").map { |c| c.titleize.strip }
50
+ end
51
+
52
+ sig { params(text: T.nilable(String)).returns(T.nilable(String)) }
53
+ def combine_ordinal_string(text)
54
+ text&.gsub!(ORDINAL_REGEX) { "#{::Regexp.last_match(1)}#{::Regexp.last_match(2)}" }
55
+ text
56
+ end
57
+
58
+ sig { params(text: T.nilable(String)).returns(T.nilable(String)) }
59
+ def strip_extra_spaces(text)
60
+ text&.strip&.gsub(/\s+/, " ")
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -1,10 +1,15 @@
1
1
  id: US
2
- validation:
3
- enabled: true
4
- default_matching_strategy: es_street
5
- address_parser: AtlasEngine::ValidationTranscriber::AddressParserNorthAmerica
6
2
  ingestion:
7
3
  settings:
8
4
  number_of_shards: "7"
9
5
  min_zip_edge_ngram: "1"
10
6
  max_zip_edge_ngram: "10"
7
+ post_address_mapper:
8
+ open_address: AtlasEngine::Us::AddressImporter::OpenAddress::Mapper
9
+ validation:
10
+ enabled: true
11
+ default_matching_strategy: es_street
12
+ unmatched_components_suggestion_threshold: 1
13
+ address_parser: AtlasEngine::ValidationTranscriber::AddressParserNorthAmerica
14
+ open_address:
15
+ filter: AtlasEngine::Us::AddressImporter::OpenAddress::Filter
@@ -0,0 +1,120 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ require "zip"
5
+
6
+ module AtlasEngine
7
+ module Us
8
+ module Jobs
9
+ module AddressImporter
10
+ class CombinedImportJob < ApplicationJob
11
+ include ::AtlasEngine::AddressImporter::ImportLogHelper
12
+ discard_on Exception
13
+ rescue_from(ArgumentError) { raise }
14
+
15
+ COUNTRY_CODE = "US"
16
+ LOCALE = "en"
17
+ US_STATES = [
18
+ "AK",
19
+ "AL",
20
+ "AR",
21
+ "AZ",
22
+ "CA",
23
+ "CO",
24
+ "CT",
25
+ "DC",
26
+ "DE",
27
+ "FL",
28
+ "GA",
29
+ "HI",
30
+ "IA",
31
+ "ID",
32
+ "IL",
33
+ "IN",
34
+ "KS",
35
+ "KY",
36
+ "LA",
37
+ "MA",
38
+ "MD",
39
+ "ME",
40
+ "MI",
41
+ "MN",
42
+ "MO",
43
+ "MS",
44
+ "MT",
45
+ "NC",
46
+ "ND",
47
+ "NE",
48
+ "NH",
49
+ "NJ",
50
+ "NM",
51
+ "NV",
52
+ "NY",
53
+ "OH",
54
+ "OK",
55
+ "OR",
56
+ "PA",
57
+ "RI",
58
+ "SC",
59
+ "SD",
60
+ "TN",
61
+ "TX",
62
+ "UT",
63
+ "VA",
64
+ "VT",
65
+ "WA",
66
+ "WI",
67
+ "WV",
68
+ "WY",
69
+ ]
70
+
71
+ def perform(geojson_directory:)
72
+ country_import = AtlasEngine::CountryImport.create!(country_code: COUNTRY_CODE)
73
+ country_import.start!
74
+
75
+ import_log_info(
76
+ country_import: country_import,
77
+ message: "Starting import for #{COUNTRY_CODE} from #{geojson_directory}",
78
+ notify: true,
79
+ )
80
+
81
+ jobs_to_run = job_list(geojson_directory, country_import)
82
+ first_job = jobs_to_run.shift
83
+ first_job[:job_name].perform_later(**first_job[:job_args].merge(followed_by: jobs_to_run))
84
+ end
85
+
86
+ private
87
+
88
+ def job_list(geojson_directory, country_import)
89
+ job_list = [
90
+ clear_records_job(country_import),
91
+ ]
92
+
93
+ US_STATES.each do |us_state|
94
+ job_list <<
95
+ {
96
+ job_name: ::AtlasEngine::AddressImporter::OpenAddress::GeoJsonImportJob,
97
+ job_args: {
98
+ country_code: COUNTRY_CODE,
99
+ province_code: us_state.upcase,
100
+ locale: LOCALE,
101
+ geojson_file_path: File.join(geojson_directory, "us-#{us_state.downcase}-statewide.geojson.gz"),
102
+ country_import_id: country_import.id,
103
+ },
104
+ }
105
+ end
106
+
107
+ job_list
108
+ end
109
+
110
+ def clear_records_job(country_import)
111
+ {
112
+ job_name: AtlasEngine::AddressImporter::ClearRecordsJob,
113
+ job_args: { country_code: COUNTRY_CODE, country_import_id: country_import.id },
114
+ }
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -6,8 +6,12 @@ input AddressInput {
6
6
  address2: String
7
7
  city: String
8
8
  countryCode: ValidationSupportedCountry
9
+ line2: String
10
+ neighborhood: String
9
11
  phone: String
10
12
  provinceCode: String
13
+ streetName: String
14
+ streetNumber: String
11
15
  zip: String
12
16
  }
13
17
 
@@ -12,7 +12,11 @@ module AtlasEngine
12
12
  description "Address fields used to fulfill a validation request"
13
13
 
14
14
  argument :address1, String, required: false
15
+ argument :street_name, String, required: false
16
+ argument :street_number, String, required: false
15
17
  argument :address2, String, required: false
18
+ argument :line2, String, required: false
19
+ argument :neighborhood, String, required: false
16
20
  argument :city, String, required: false
17
21
  argument :country_code, ValidationSupportedCountry, required: false
18
22
  argument :province_code, String, required: false
@@ -24,7 +24,7 @@ module AtlasEngine
24
24
  extend T::Sig
25
25
  include HandlesInterruption
26
26
  include PreparesGeoJsonFile
27
- attr_reader :geojson_path, :country_import, :country_code, :loader, :transformer
27
+ attr_reader :geojson_path, :country_import, :country_code, :province_code, :loader, :transformer
28
28
 
29
29
  CHUNK_SIZE = 10_000
30
30
  REPORT_STEP = 5
@@ -36,10 +36,11 @@ module AtlasEngine
36
36
  def setup_and_download(&block)
37
37
  @loader = Loader.new
38
38
  @country_code = argument(:country_code)
39
+ @province_code = argument(:province_code)
39
40
  @geojson_path = Pathname.new(argument(:geojson_file_path))
40
41
  @locale = argument(:locale)&.downcase
41
42
  @country_import = CountryImport.find(argument(:country_import_id))
42
- @transformer = Transformer.new(country_import: country_import, locale: @locale)
43
+ @transformer = Transformer.new(country_import: country_import, province_code: @province_code, locale: @locale)
43
44
 
44
45
  import_log_info(
45
46
  country_import: country_import,
@@ -22,7 +22,11 @@ module AtlasEngine
22
22
  sig do
23
23
  params(
24
24
  address1: String,
25
+ street_name: String,
26
+ street_number: String,
25
27
  address2: String,
28
+ line2: String,
29
+ neighborhood: String,
26
30
  city: String,
27
31
  province_code: String,
28
32
  zip: String,
@@ -30,10 +34,15 @@ module AtlasEngine
30
34
  phone: String,
31
35
  ).returns(AddressValidation::Address)
32
36
  end
33
- def build_address(address1: "", address2: "", city: "", province_code: "", zip: "", country_code: "", phone: "")
37
+ def build_address(address1: "", street_name: "", street_number: "", address2: "", line2: "", neighborhood: "",
38
+ city: "", province_code: "", zip: "", country_code: "", phone: "")
34
39
  AddressValidation::Address.new(
35
40
  address1: address1,
41
+ street_name: street_name,
42
+ street_number: street_number,
36
43
  address2: address2,
44
+ line2: line2,
45
+ neighborhood: neighborhood,
37
46
  city: city,
38
47
  province_code: province_code,
39
48
  zip: zip,
@@ -7,9 +7,10 @@ module AtlasEngine
7
7
  class DefaultMapper
8
8
  extend T::Sig
9
9
  include FeatureHelper
10
- sig { params(country_code: String, locale: T.nilable(String)).void }
11
- def initialize(country_code:, locale: nil)
10
+ sig { params(country_code: String, province_code: T.nilable(String), locale: T.nilable(String)).void }
11
+ def initialize(country_code:, province_code: nil, locale: nil)
12
12
  @country_code = country_code
13
+ @province_code = province_code
13
14
  @locale = locale
14
15
  end
15
16
 
@@ -27,7 +28,7 @@ module AtlasEngine
27
28
  source_id: openaddress_source_id(feature),
28
29
  locale: @locale,
29
30
  country_code: @country_code,
30
- province_code: nil,
31
+ province_code: @province_code,
31
32
  region1: region,
32
33
  # Don't titleize. The sources have proper capitalization, and it's a problem for cities like
33
34
  # 's-Graveland, which would get titleized to "'S Graveland" which is wrong.
@@ -8,11 +8,12 @@ module AtlasEngine
8
8
  extend T::Sig
9
9
  extend T::Helpers
10
10
 
11
- def initialize(country_import:, locale: nil)
11
+ def initialize(country_import:, province_code: nil, locale: nil)
12
12
  @country_code = country_import.country_code
13
13
  @locale = locale
14
+ @province_code = province_code
14
15
  @mapper = CountryProfile.for(@country_code).ingestion.post_address_mapper("open_address").new(
15
- country_code: @country_code, locale: @locale,
16
+ country_code: @country_code, province_code: @province_code, locale: @locale,
16
17
  )
17
18
  @corrector = AddressImporter::Corrections::Corrector.new(country_code: @country_code, source: "open_address")
18
19
  @validator = AddressImporter::Validation::Wrapper.new(
@@ -164,7 +164,7 @@ module AtlasEngine
164
164
 
165
165
  sig { params(value: String).returns(T::Boolean) }
166
166
  def include_fractions?(value)
167
- %r{^([0-9]+ )?([0-9]+/[0-9]+)?$}.match?(value)
167
+ %r{^([0-9]+ )?([0-9]+/[1-9][0-9]*)?$}.match?(value)
168
168
  end
169
169
 
170
170
  sig { params(str: String).returns(String) }
@@ -13,9 +13,21 @@ module AtlasEngine
13
13
  sig { abstract.returns(ComponentType) }
14
14
  def address1; end
15
15
 
16
+ sig { returns(ComponentType) }
17
+ def street_name; end
18
+
19
+ sig { returns(ComponentType) }
20
+ def street_number; end
21
+
16
22
  sig { abstract.returns(ComponentType) }
17
23
  def address2; end
18
24
 
25
+ sig { returns(ComponentType) }
26
+ def line2; end
27
+
28
+ sig { returns(ComponentType) }
29
+ def neighborhood; end
30
+
19
31
  sig { abstract.returns(ComponentType) }
20
32
  def city; end
21
33
 
@@ -13,7 +13,11 @@ module AtlasEngine
13
13
  AddressInput = T.type_alias { Types::AddressValidation::AddressInput }
14
14
 
15
15
  const :address1, ComponentType
16
+ const :street_name, ComponentType
17
+ const :street_number, ComponentType
16
18
  const :address2, ComponentType
19
+ const :line2, ComponentType
20
+ const :neighborhood, ComponentType
17
21
  const :city, ComponentType
18
22
  const :province_code, ComponentType
19
23
  const :phone, ComponentType
@@ -33,7 +37,11 @@ module AtlasEngine
33
37
  def from_address(address:)
34
38
  new(
35
39
  address1: address.address1,
40
+ street_name: address.street_name,
41
+ street_number: address.street_number,
36
42
  address2: address.address2,
43
+ line2: address.line2,
44
+ neighborhood: address.neighborhood,
37
45
  city: address.city,
38
46
  country_code: address.country_code,
39
47
  province_code: address.province_code,
@@ -9,9 +9,21 @@ module AtlasEngine
9
9
  sig { returns(String) }
10
10
  attr_reader :address1
11
11
 
12
+ sig { returns(String) }
13
+ attr_reader :street_name
14
+
15
+ sig { returns(String) }
16
+ attr_reader :street_number
17
+
12
18
  sig { returns(String) }
13
19
  attr_reader :address2
14
20
 
21
+ sig { returns(String) }
22
+ attr_reader :line2
23
+
24
+ sig { returns(String) }
25
+ attr_reader :neighborhood
26
+
15
27
  sig { returns(String) }
16
28
  attr_reader :city
17
29
 
@@ -73,7 +85,11 @@ module AtlasEngine
73
85
  timestamp: Time,
74
86
  origin: String,
75
87
  address1: String,
88
+ street_name: String,
89
+ street_number: String,
76
90
  address2: String,
91
+ line2: String,
92
+ neighborhood: String,
77
93
  city: String,
78
94
  province_code: String,
79
95
  country_code: String,
@@ -87,7 +103,11 @@ module AtlasEngine
87
103
  timestamp: Time.zone.now,
88
104
  origin: "",
89
105
  address1: "",
106
+ street_name: "",
107
+ street_number: "",
90
108
  address2: "",
109
+ line2: "",
110
+ neighborhood: "",
91
111
  city: "",
92
112
  province_code: "",
93
113
  country_code: "",
@@ -100,7 +120,11 @@ module AtlasEngine
100
120
  @timestamp = timestamp
101
121
  @origin = origin
102
122
  @address1 = address1
123
+ @street_name = street_name
124
+ @street_number = street_number
103
125
  @address2 = address2
126
+ @line2 = line2
127
+ @neighborhood = neighborhood
104
128
  @city = city
105
129
  @province_code = province_code
106
130
  @country_code = country_code
@@ -114,7 +138,11 @@ module AtlasEngine
114
138
  def address_attributes
115
139
  {
116
140
  address1: address1,
141
+ street_name: street_name,
142
+ street_number: street_number,
117
143
  address2: address2,
144
+ line2: line2,
145
+ neighborhood: neighborhood,
118
146
  city: city,
119
147
  province_code: province_code,
120
148
  zip: zip,
@@ -114,6 +114,8 @@ module AtlasEngine
114
114
  local_concerns = {}
115
115
  cache = Validators::Predicates::Cache.new(pipeline_address)
116
116
  @predicate_pipeline.pipeline.each do |config|
117
+ break if local_concerns[:country].present?
118
+
117
119
  local_concerns[config.field] = [] if local_concerns[config.field].nil?
118
120
  next if local_concerns[config.field].present?
119
121
 
@@ -12,7 +12,11 @@ module AtlasEngine
12
12
  params(
13
13
  address: {
14
14
  address1: T.nilable(String),
15
+ street_name: T.nilable(String),
16
+ street_number: T.nilable(String),
15
17
  address2: T.nilable(String),
18
+ line2: T.nilable(String),
19
+ neighborhood: T.nilable(String),
16
20
  city: T.nilable(String),
17
21
  province_code: T.nilable(String),
18
22
  country_code: T.nilable(String),
@@ -8,7 +8,7 @@ module AtlasEngine
8
8
  class UnmatchedFieldConcernBuilder
9
9
  extend T::Sig
10
10
  include ConcernFormatter
11
- attr_reader :address, :component, :matched_components, :unmatched_field
11
+ attr_reader :address, :unmatched_component, :matched_components, :unmatched_field
12
12
 
13
13
  COMPONENTS_TO_LABELS = {
14
14
  zip: "ZIP",
@@ -31,9 +31,9 @@ module AtlasEngine
31
31
  end
32
32
  def initialize(unmatched_component, matched_components, address, unmatched_field = nil)
33
33
  @address = address
34
- @component = unmatched_component
34
+ @unmatched_component = unmatched_component
35
35
  @matched_components = matched_components
36
- @unmatched_field = unmatched_field
36
+ @unmatched_field = unmatched_field || unmatched_component
37
37
  end
38
38
 
39
39
  sig do
@@ -61,7 +61,7 @@ module AtlasEngine
61
61
 
62
62
  sig { returns(Symbol) }
63
63
  def code
64
- "#{shortened_component_name}_inconsistent".to_sym
64
+ "#{unmatched_component_name}_inconsistent".to_sym
65
65
  end
66
66
 
67
67
  sig { returns(T::Array[Symbol]) }
@@ -69,21 +69,14 @@ module AtlasEngine
69
69
  [field_name]
70
70
  end
71
71
 
72
- sig { returns(T::Array[String]) }
73
- def valid_address_component_values
74
- matched_components.last(2).map do |component|
75
- component == :province_code ? province_name : address[component]
76
- end
77
- end
78
-
79
72
  sig { returns(Symbol) }
80
- def shortened_component_name
81
- SHORTENED_COMPONENT_NAMES[component] || component
73
+ def unmatched_component_name
74
+ SHORTENED_COMPONENT_NAMES[unmatched_component] || unmatched_component
82
75
  end
83
76
 
84
77
  sig { returns(Symbol) }
85
78
  def field_name
86
- unmatched_field || shortened_component_name
79
+ SHORTENED_COMPONENT_NAMES[unmatched_field] || unmatched_field
87
80
  end
88
81
  end
89
82
  end
@@ -9,7 +9,6 @@ module AtlasEngine
9
9
  class Present < Predicate
10
10
  sig { override.returns(T.nilable(Concern)) }
11
11
  def evaluate
12
- return unless @cache.country.country?
13
12
  return if @cache.country.field(key: :city).autofill(locale: :en).present?
14
13
 
15
14
  build_concern if @address.city.blank?
@@ -8,8 +8,6 @@ module AtlasEngine
8
8
  class NoEmojis < Predicate
9
9
  sig { override.returns(T.nilable(Concern)) }
10
10
  def evaluate
11
- return unless @cache.country.country?
12
-
13
11
  build_concern if contains_blocked_codepoints?(address.send(@field))
14
12
  end
15
13
 
@@ -8,8 +8,6 @@ module AtlasEngine
8
8
  class NoHtmlTags < Predicate
9
9
  sig { override.returns(T.nilable(Concern)) }
10
10
  def evaluate
11
- return unless @cache.country.country?
12
-
13
11
  build_concern if contains_html_tags(address.send(@field))
14
12
  end
15
13
 
@@ -8,8 +8,6 @@ module AtlasEngine
8
8
  class NoUrl < Predicate
9
9
  sig { override.returns(T.nilable(Concern)) }
10
10
  def evaluate
11
- return unless @cache.country.country?
12
-
13
11
  build_concern if contains_url(address.send(@field))
14
12
  end
15
13
 
@@ -9,8 +9,6 @@ module AtlasEngine
9
9
  MAX_COMPONENT_LENGTH = 255
10
10
  sig { override.returns(T.nilable(Concern)) }
11
11
  def evaluate
12
- return unless @cache.country.country?
13
-
14
12
  build_concern if address.send(@field).to_s.length > MAX_COMPONENT_LENGTH
15
13
  end
16
14
 
@@ -9,8 +9,6 @@ module AtlasEngine
9
9
  class Valid < Predicate
10
10
  sig { override.returns(T.nilable(Concern)) }
11
11
  def evaluate
12
- return if @address.country_code.blank?
13
- return unless @cache.country.country?
14
12
  return if @address.phone.blank?
15
13
 
16
14
  phone = Worldwide::Phone.new(number: @address.phone, country_code: @address.country_code)
@@ -9,7 +9,6 @@ module AtlasEngine
9
9
  class Exists < Predicate
10
10
  sig { override.returns(T.nilable(Concern)) }
11
11
  def evaluate
12
- return unless @cache.country.country?
13
12
  return if address.province_code.present? ||
14
13
  country_has_no_provinces ||
15
14
  @cache.country.province_optional?
@@ -9,7 +9,6 @@ module AtlasEngine
9
9
  class ValidForCountry < Predicate
10
10
  sig { override.returns(T.nilable(Concern)) }
11
11
  def evaluate
12
- return unless @cache.country.country?
13
12
  return if address.province_code.blank?
14
13
  return if @cache.country.zones.none?(&:province?)
15
14
  return if @cache.country.hide_provinces_from_addresses
@@ -9,8 +9,6 @@ module AtlasEngine
9
9
  class BuildingNumberInAddress1 < Predicate
10
10
  sig { override.returns(T.nilable(Concern)) }
11
11
  def evaluate
12
- return unless @cache.country.country?
13
-
14
12
  return unless @cache.country.building_number_required
15
13
  return if @cache.country.building_number_may_be_in_address2
16
14
 
@@ -9,7 +9,6 @@ module AtlasEngine
9
9
  class BuildingNumberInAddress1OrAddress2 < Predicate
10
10
  sig { override.returns(T.nilable(Concern)) }
11
11
  def evaluate
12
- return unless @cache.country.country?
13
12
  return unless @cache.country.building_number_required
14
13
  return unless @cache.country.building_number_may_be_in_address2
15
14
  return if contains_number?(T.must(@address.address1)) || contains_number?(@address.address2)
@@ -9,8 +9,6 @@ module AtlasEngine
9
9
  class Present < Predicate
10
10
  sig { override.returns(T.nilable(Concern)) }
11
11
  def evaluate
12
- return unless @cache.country.country?
13
-
14
12
  build_concern if @address.address1.blank?
15
13
  end
16
14
 
@@ -11,7 +11,6 @@ module AtlasEngine
11
11
 
12
12
  sig { returns(T.nilable(T::Boolean)) }
13
13
  def concerning?
14
- return false unless @cache.country.country?
15
14
  return false unless @cache.country.has_zip?
16
15
  return false unless @cache.country.zip_required?
17
16
 
@@ -12,9 +12,6 @@ module Maintenance
12
12
  # ISO3166 two-letter country code.
13
13
  attribute :country_code, :string
14
14
  validates :country_code, presence: true
15
- # Filename to import. When running in staging or production, the worker expects to find
16
- # this file in the relevant GCS bucket, configured in `config/storage/{environment}.yml`
17
- # It must be placed under `openaddress/` with the same filename.
18
15
  attribute :geojson_file_path, :string
19
16
  attribute :locale, :string
20
17
 
@@ -0,0 +1,20 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ module Maintenance
5
+ module AtlasEngine
6
+ class UsGeoJsonDirectoryImportTask < MaintenanceTasks::Task
7
+ include ::AtlasEngine::HandlesBlob
8
+
9
+ no_collection
10
+
11
+ attribute :geojson_directory, :string
12
+
13
+ def process
14
+ ::AtlasEngine::Us::Jobs::AddressImporter::CombinedImportJob.perform_later(
15
+ geojson_directory: geojson_directory.strip,
16
+ )
17
+ end
18
+ end
19
+ end
20
+ end
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module AtlasEngine
5
- VERSION = "0.8.0"
5
+ VERSION = "1.1.0"
6
6
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: atlas_engine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-04-02 00:00:00.000000000 Z
11
+ date: 2024-05-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: annex_29
@@ -178,6 +178,20 @@ dependencies:
178
178
  - - ">="
179
179
  - !ruby/object:Gem::Version
180
180
  version: '0'
181
+ - !ruby/object:Gem::Dependency
182
+ name: sprockets-rails
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">="
186
+ - !ruby/object:Gem::Version
187
+ version: '0'
188
+ type: :runtime
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ version: '0'
181
195
  - !ruby/object:Gem::Dependency
182
196
  name: state_machines-activerecord
183
197
  requirement: !ruby/object:Gem::Requirement
@@ -364,7 +378,10 @@ files:
364
378
  - app/countries/atlas_engine/si/validation_transcriber/address_parser.rb
365
379
  - app/countries/atlas_engine/tt/address_importer/open_address/mapper.rb
366
380
  - app/countries/atlas_engine/tt/country_profile.yml
381
+ - app/countries/atlas_engine/us/address_importer/open_address/filter.rb
382
+ - app/countries/atlas_engine/us/address_importer/open_address/mapper.rb
367
383
  - app/countries/atlas_engine/us/country_profile.yml
384
+ - app/countries/atlas_engine/us/jobs/address_importer/combined_import_job.rb
368
385
  - app/countries/atlas_engine/us/synonyms.yml
369
386
  - app/graphql/atlas_engine/errors/locale_unsupported_error.rb
370
387
  - app/graphql/atlas_engine/schema.graphql
@@ -548,6 +565,7 @@ files:
548
565
  - app/models/atlas_engine/street.rb
549
566
  - app/tasks/maintenance/atlas_engine/elasticsearch_index_create_task.rb
550
567
  - app/tasks/maintenance/atlas_engine/geo_json_import_task.rb
568
+ - app/tasks/maintenance/atlas_engine/us_geo_json_directory_import_task.rb
551
569
  - app/views/atlas_engine/connectivity/index.html.erb
552
570
  - app/views/atlas_engine/country_imports/index.html.erb
553
571
  - app/views/atlas_engine/country_imports/show.html.erb
@@ -597,7 +615,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
597
615
  - !ruby/object:Gem::Version
598
616
  version: '0'
599
617
  requirements: []
600
- rubygems_version: 3.5.7
618
+ rubygems_version: 3.5.10
601
619
  signing_key:
602
620
  specification_version: 4
603
621
  summary: Address Validation API