parseapi 1.9.2 → 1.10.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9305f04eec9c9f1ed484906fb69244df03127edae6b7466c023e5855631ec6dd
4
- data.tar.gz: 2adccd9a7bfc7a456b7f0b4b5bab86f3d8b30bb4fa744b2c510a14933b99b4bf
3
+ metadata.gz: 668ab627de5bf1a6b476e54841045fe6e3110d1ccd91e861380ea21d5103be40
4
+ data.tar.gz: a9cf7d1cc8d1b28baa3a62f29355deff8cda22eb1ee27a95b328f78dc2efe646
5
5
  SHA512:
6
- metadata.gz: 4f71799f8c0304a0281fd4b3e50c08d66e4123243515eb85d0077acedbd420bb753ac5a67040e73382005d88cb8f7fd891cc495af7c958c553b8206399e8bf7f
7
- data.tar.gz: 98f12a7c2d8ef6886cd78587a9a2578c1858aa85bf33e1be6d2ba407d2185f705a0e385b1d6c1da010246b2750c9614850a8f9d7c3a1d580290aa47d8f396a4b
6
+ metadata.gz: 58883fd322a90aba10e64f12299b785313105d589f02cb627b5c41cb3870b8feedc80c9efefb061b8dc7779cbc34d4c941813ff49ad0e135f58ef10a5842dd5e
7
+ data.tar.gz: 81b76b6ffd9b1e35f320f59a553564671f74fb0275364503b77bce581ac8b07651e91a9d30fc4f0d22a9cfc4181d48616585dbee6597d6d0a6978c79973270f5
data/README.md CHANGED
@@ -243,7 +243,7 @@ place = parse.postal('28202', country: 'US', deep: true)
243
243
  property_tax = place.dig('deep', 'property_tax')
244
244
  ```
245
245
 
246
- Read `population_period` alongside `population`: a reporting year (`YYYY`) or period (`YYYY-YYYY`), null when unknown or unverifiable. Keep missing or null values unknown and preserve a known zero. These fields belong to full place profiles. State district lists include each district's population and period. Postal nearby and distance detail remains metropolitan associations only. Continent population stays in core.
246
+ Read `population` as the source’s count for that place boundary. Keep missing or null values unknown and preserve a known zero. State district lists include each district’s population. Postal nearby and distance detail remains metropolitan associations only. Continent population stays in core.
247
247
 
248
248
  Country deep includes `land_area` and `water_area` in km2, `coastline` in km, and mean `elevation` in metres. `lowest_point` and `highest_point` contain a nullable `name` and an `elevation` in metres. Values below sea level are negative. Missing or null values stay unknown, and zero stays zero.
249
249
 
@@ -257,7 +257,7 @@ parse.weather(40.7128, -74.006, deep: true, date: '2026-08-15')
257
257
 
258
258
  Tariff starts with the general schedule line. Paid deep adds units and the special and other schedule columns. An optional origin then resolves country-specific measures. The three calls below show those successive choices. Without origin, schedule detail is still returned and origin-dependent fields are null. A null effective rate is not a zero rate.
259
259
 
260
- Tariff lookup and search accept an optional `edition` fingerprint and `date` (`YYYY-MM-DD`). The edition pins exact immutable source bytes. A date is accepted only when verified source coverage exists. An edition without a date returns undated schedule context (`date: null`). Default requests use today. Paid detail exposes an open-string `reason` when `effective_rate` is null, including `incomplete_coverage`. A null rate never means zero. Explicit selections fail with `tariff_selection_mismatch` if an older server ignores the requested scope.
260
+ Tariff lookup and search accept the `edition` ID returned by Tariff, such as `ted_3`, and an optional `date` (`YYYY-MM-DD`). The edition pins exact immutable source bytes. A date is accepted only when verified source coverage exists. An edition without a date returns undated schedule context (`date: null`). Default requests use today. Paid detail exposes an open-string `reason` when `effective_rate` is null, including `incomplete_coverage`. A null rate never means zero. Explicit selections fail with `tariff_selection_mismatch` if an older server ignores the requested scope.
261
261
 
262
262
  Origin means where the goods originate, not where they ship from. The effective rate covers matched stored schedule measures only. It is not complete duty or landed cost.
263
263
 
@@ -297,7 +297,7 @@ Invalid input returns `valid: false` with unknown provider fields. A checksum-va
297
297
 
298
298
  The default pooled lookup includes provider identity, specialty and practice contact where held. Paid `deep` adds `deactivated_at`, `medicare`, `opt_out` and `enrollments` from stored source files, with no separate check meter or live verification. `enrollments: null` means unavailable; `[]` means no enrollment rows are returned. The API omits unrequested `deep` and returns `{}` when requested on Free.
299
299
 
300
- Paid Deep also returns `taxonomies` in published order, with taxonomy code, specialty label, primary flag and provider-reported license number/state, plus `enumerated_at`, `updated_at` and `reactivated_at` record dates. Reported licenses are not verified licenses. Null lists mean unavailable; empty lists mean the edition contains no entries. Core `sources` is available on every plan: NPPES, LEIE, PECOS and opt-out each have nullable edition metadata (`edition`, `published_at`, `through`, `imported_at`). Provider record dates are separate from source publication and completed import dates. Older responses may omit these additions. Edition details remain null until a verified source is served.
300
+ Paid Deep also returns `taxonomies` in published order, with taxonomy code, specialty label, primary flag and provider-reported license number/state, plus `enumerated_at`, `updated_at` and `reactivated_at` record dates. Reported licenses are not verified licenses. Null lists mean unavailable; empty lists mean the edition contains no entries. Provider record dates describe the provider record. Unknown dates stay null.
301
301
 
302
302
  ## Deep
303
303
 
@@ -453,3 +453,53 @@ A device's emoji font remains the ordinary display.
453
453
 
454
454
  These additions are a local release candidate until the matching SDK and
455
455
  backend are published. Existing lookup and search contracts are preserved.
456
+
457
+
458
+ ## City collections
459
+
460
+ ```ruby
461
+ page = parse.city_list(country: 'JP', min_population: 100_000, sort: 'population', limit: 20)
462
+ nearby = parse.city_nearby_coords(34.57, 135.48, radius: 20, unit: 'km', country: 'JP')
463
+ search = parse.city_search('Sak', page: true)
464
+ ```
465
+
466
+ Browse the curated City catalog or list cities around a supplied coordinate. These operations return `next_cursor`, which is null at the end. Treat it as opaque and reuse all filters, limit, language, hierarchy and deep settings for the next page. Existing name searches and named nearby calls include pagination metadata only when paging or a cursor is requested.
467
+
468
+ Collection options include minimum and maximum population, an exact IANA timezone, and `bbox` in `west,south,east,north` order. A west bound greater than east crosses the dateline. Population filtering and sorting use the same count as paid Deep: an admitted observation when available, otherwise the stored count. Missing counts do not match a population range and sort last; legacy counts keep unknown dates. Coverage and observation dates vary by place.
469
+
470
+ Collections support `name`, `population`, `country` and `elevation`; search also supports `relevance`, and nearby supports `distance`. Combine up to three signed terms, for example `sort=-population,name`. A signed multi-key sort does not also take `order`. Population defaults to descending order; other sorts default to ascending. Set `order` to `asc` or `desc`, except relevance only supports ascending. Named nearby uses country and state to select the anchor; population, timezone and bounding box filter the returned neighbors. Coordinate nearby applies country and state to its returned cities.
471
+
472
+
473
+
474
+ ## Canonical City operations
475
+
476
+ Classic lookup, search, nearest and named-nearby methods retain their default envelopes and required-field models. Browse and coordinate-nearby use canonical lists: `object`, `data` and nullable `next_cursor` (camelCase properties in Swift/Kotlin). Keep all query options unchanged when continuing a page; no client fetches pages automatically.
477
+
478
+ ```ruby
479
+ page = parse.city_search('Viena', format: 'list', match: 'fuzzy', max_distance: 1)
480
+ children = parse.city_children('city_7aj899nvwyku')
481
+ distance = parse.city_distance('city_7aj899nvwyku', 'city_7aj899nvwyku')
482
+ ```
483
+
484
+ Select `format=resource` for one City or `format=list` for a collection. Country inclusion/exclusion sets, timezone sets, inclusive population and meter-elevation bounds, stored-center `bbox` and signed multi-key sorting are available for collections. Unknown numeric facts do not match range filters.
485
+
486
+ Search defaults to prefix matching. Exact and fuzzy matching are explicit; fuzzy accepts maximum edit distance 1 or 2 and returns `match` evidence (`type`, `source`, `value`, `language`, `edit_distance`). Review a fuzzy choice before storing its ID.
487
+
488
+ Reviewed children, ancestors and siblings use canonical lists with partial relationship coverage. Empty data does not prove there are no geographic relatives. Children/siblings accept page controls; ancestors do not. Distance reports great-circle kilometers and miles between stored centers: zero is known zero, null is unknown, and it does not imply travel time or containment.
489
+
490
+ Strict `ambiguity=error` can return HTTP 409 `ambiguous_city` with up to two safe candidate identities. The existing error constructor and unrelated error metadata are preserved; candidates are retained only for that status/code and never contain Deep. GeoJSON is available through HTTP; these typed City list methods do not return GeoJSON.
491
+
492
+
493
+
494
+ Reviewed name forms:
495
+
496
+ ```ruby
497
+ city = parse.city_id('city_7aj899nvwyku', format: 'resource', romanized: true)
498
+ ```
499
+
500
+ Coverage by country and available fields is described at https://parseapi.com/city#coverage.
501
+
502
+ `romanized=true` requests reviewed official name forms as `name_romanized` and `romanization_language` (camelCase in Swift/Kotlin). Current reviewed forms cover 20 Japanese whole-city aliases in ja-Latn. Missing or conflicting forms stay null; ASCII folding is not substituted for an official form.
503
+
504
+
505
+ Conditional `If-None-Match` reads and GeoJSON currently use HTTP directly. A matching City ETag returns 304 without a body; ordinary billing and rate limits apply. These JSON client methods do not supply conditional-cache handling.
@@ -6,7 +6,7 @@ require 'time'
6
6
  module ParseAPI
7
7
  # Every non-2xx response from the API. Branch on +code+, never on the message.
8
8
  class Error < StandardError
9
- attr_reader :status, :code, :docs, :request_id, :retry_after
9
+ attr_reader :status, :code, :docs, :request_id, :retry_after, :candidates
10
10
 
11
11
  def initialize(status:, code:, message:, docs: nil, request_id: nil, retry_after: nil)
12
12
  super(message)
@@ -106,28 +106,69 @@ module ParseAPI
106
106
  get("/district/#{seg(code)}", country: country, state: state, deep: deep, lang: lang)
107
107
  end
108
108
 
109
- def city(name, country: nil, state: nil, deep: false, lang: nil)
110
- get("/city/#{seg(name)}", country: country, state: state, deep: deep, lang: lang)
109
+ def city(name, country: nil, state: nil, deep: false, lang: nil, type: nil, grain: nil, hierarchy: false, aliases: false, ambiguity: nil, format: nil, romanized: false)
110
+ get("/city/#{seg(name)}", country: country, state: state, deep: deep, lang: lang, type: type, grain: grain, hierarchy: hierarchy, aliases: aliases, ambiguity: ambiguity, romanized: romanized, format: format)
111
111
  end
112
112
 
113
- def city_id(id, deep: false, lang: nil)
114
- get("/city/id/#{seg(id)}", deep: deep, lang: lang)
113
+ def city_id(id, deep: false, lang: nil, hierarchy: false, format: nil, romanized: false)
114
+ get("/city/id/#{seg(id)}", deep: deep, lang: lang, hierarchy: hierarchy, romanized: romanized, format: format)
115
115
  end
116
116
 
117
- def city_search(query, country: nil, state: nil, limit: nil, deep: false, lang: nil)
118
- get('/city', q: query, country: country, state: state, limit: limit, deep: deep, lang: lang)
117
+ def city_search(query, country: nil, state: nil, limit: nil, deep: false, lang: nil, type: nil, grain: nil, hierarchy: false, aliases: false, min_population: nil, max_population: nil, timezone: nil, bbox: nil, sort: nil, order: nil, page: false, cursor: nil, format: nil, countries: nil, exclude_countries: nil, timezones: nil, min_elevation: nil, max_elevation: nil, match: nil, max_distance: nil, romanized: false)
118
+ spelling = !match.nil? || !max_distance.nil?
119
+ response = get(spelling ? '/city/search' : '/city', q: query, match: match, max_distance: max_distance, country: country, state: state, limit: limit, deep: deep, lang: lang, type: type, grain: grain, hierarchy: hierarchy, aliases: aliases, min_population: min_population, max_population: max_population, timezone: timezone, bbox: bbox, sort: sort, order: order, page: page, cursor: cursor, romanized: romanized, format: format, countries: city_csv(countries), exclude_countries: city_csv(exclude_countries), timezones: city_csv(timezones), min_elevation: min_elevation, max_elevation: max_elevation)
120
+ if spelling && format.nil?
121
+ classic = {"q"=>query, "cities"=>response.fetch("data"), "next_cursor"=>response["next_cursor"]}
122
+ classic["country"] = country unless country.nil?
123
+ classic["state"] = state unless state.nil?
124
+ return classic
125
+ end
126
+ response
119
127
  end
120
128
 
121
- def city_nearest(lat, lon, deep: false, lang: nil)
122
- get('/city', lat: lat, lon: lon, deep: deep, lang: lang)
129
+ def city_nearest(lat, lon, deep: false, lang: nil, country: nil, state: nil, type: nil, grain: nil, hierarchy: false, format: nil, romanized: false)
130
+ get('/city', lat: lat, lon: lon, deep: deep, lang: lang, country: country, state: state, type: type, grain: grain, hierarchy: hierarchy, romanized: romanized, format: format)
123
131
  end
124
132
 
125
- def city_nearby(name, radius: nil, unit: nil, country: nil, state: nil, limit: nil, deep: false, lang: nil)
126
- get("/city/#{seg(name)}/nearby", radius: radius, unit: unit, country: country, state: state, limit: limit, deep: deep, lang: lang)
133
+ def city_nearby(name, radius: nil, unit: nil, country: nil, state: nil, limit: nil, deep: false, lang: nil, type: nil, grain: nil, hierarchy: false, aliases: false, ambiguity: nil, min_population: nil, max_population: nil, timezone: nil, bbox: nil, sort: nil, order: nil, page: false, cursor: nil, format: nil, countries: nil, exclude_countries: nil, timezones: nil, min_elevation: nil, max_elevation: nil, romanized: false)
134
+ get("/city/#{seg(name)}/nearby", radius: radius, unit: unit, country: country, state: state, limit: limit, deep: deep, lang: lang, type: type, grain: grain, hierarchy: hierarchy, aliases: aliases, ambiguity: ambiguity, min_population: min_population, max_population: max_population, timezone: timezone, bbox: bbox, sort: sort, order: order, page: page, cursor: cursor, romanized: romanized, format: format, countries: city_csv(countries), exclude_countries: city_csv(exclude_countries), timezones: city_csv(timezones), min_elevation: min_elevation, max_elevation: max_elevation)
127
135
  end
128
136
 
129
137
  # Look up a postal area. Pass country when known. Check nullable coordinates before another
130
138
  # location lookup.
139
+ def city_ancestors(id, hierarchy: false, deep: false, lang: nil, romanized: false)
140
+ get("/city/id/#{seg(id)}/ancestors", romanized: romanized, hierarchy: hierarchy, deep: deep, lang: lang)
141
+ end
142
+
143
+
144
+
145
+
146
+ def city_children(id, limit: nil, cursor: nil, hierarchy: false, deep: false, lang: nil, romanized: false)
147
+ get("/city/id/#{seg(id)}/children", limit: limit, cursor: cursor, romanized: romanized, hierarchy: hierarchy, deep: deep, lang: lang)
148
+ end
149
+
150
+
151
+
152
+ def city_distance(from, to)
153
+ get("/city/id/#{seg(from)}/distance/#{seg(to)}")
154
+ end
155
+
156
+
157
+ def city_list(country: nil, state: nil, limit: nil, deep: false, lang: nil, type: nil, grain: nil, hierarchy: false, min_population: nil, max_population: nil, timezone: nil, bbox: nil, sort: nil, order: nil, cursor: nil, countries: nil, exclude_countries: nil, timezones: nil, min_elevation: nil, max_elevation: nil, romanized: false)
158
+ get('/city', browse: true, country: country, state: state, limit: limit, deep: deep, lang: lang, type: type, grain: grain, hierarchy: hierarchy, min_population: min_population, max_population: max_population, timezone: timezone, bbox: bbox, sort: sort, order: order, cursor: cursor, romanized: romanized, format: 'list', countries: city_csv(countries), exclude_countries: city_csv(exclude_countries), timezones: city_csv(timezones), min_elevation: min_elevation, max_elevation: max_elevation)
159
+ end
160
+
161
+
162
+ def city_nearby_coords(lat, lon, radius: nil, unit: nil, country: nil, state: nil, limit: nil, deep: false, lang: nil, type: nil, grain: nil, hierarchy: false, min_population: nil, max_population: nil, timezone: nil, bbox: nil, sort: nil, order: nil, cursor: nil, countries: nil, exclude_countries: nil, timezones: nil, min_elevation: nil, max_elevation: nil, romanized: false)
163
+ get('/city', lat: lat, lon: lon, nearby: true, radius: radius, unit: unit, country: country, state: state, limit: limit, deep: deep, lang: lang, type: type, grain: grain, hierarchy: hierarchy, min_population: min_population, max_population: max_population, timezone: timezone, bbox: bbox, sort: sort, order: order, cursor: cursor, romanized: romanized, format: 'list', countries: city_csv(countries), exclude_countries: city_csv(exclude_countries), timezones: city_csv(timezones), min_elevation: min_elevation, max_elevation: max_elevation)
164
+ end
165
+
166
+
167
+ def city_siblings(id, limit: nil, cursor: nil, hierarchy: false, deep: false, lang: nil, romanized: false)
168
+ get("/city/id/#{seg(id)}/siblings", limit: limit, cursor: cursor, romanized: romanized, hierarchy: hierarchy, deep: deep, lang: lang)
169
+ end
170
+
171
+
131
172
  def postal(code, country: nil, deep: false, lang: nil)
132
173
  get("/postal/#{seg(code)}", country: country, deep: deep, lang: lang)
133
174
  end
@@ -465,8 +506,12 @@ module ParseAPI
465
506
  targets.join(',')
466
507
  end
467
508
 
509
+ def tariff_edition_id?(value)
510
+ value.is_a?(String) && (/\A[a-f0-9]{64}\z/.match?(value) || (/\Ated_[1-9][0-9]{0,19}\z/.match?(value) && (value.bytesize < 24 || value[4..-1] <= '18446744073709551615')))
511
+ end
512
+
468
513
  def tariff_selection(result, edition, date)
469
- if (!edition.nil? || !date.nil?) && (!result['edition'].is_a?(String) || !/\A[a-f0-9]{64}\z/.match?(result['edition']) || (!edition.nil? && result['edition'] != edition) || result['date'] != date)
514
+ if (!edition.nil? || !date.nil?) && (!tariff_edition_id?(result['edition']) || (!edition.nil? && result['edition'] != edition) || result['date'] != date)
470
515
  raise Error.new(status: 0, code: 'tariff_selection_mismatch', message: 'Tariff response did not confirm the requested edition/date. The server may not support this selection.')
471
516
  end
472
517
  result
@@ -572,6 +617,11 @@ module ParseAPI
572
617
  metered ? 0 : DEFAULT_RETRIES
573
618
  end
574
619
 
620
+ def city_csv(value)
621
+ value.is_a?(Array) ? value.join(',') : value
622
+ end
623
+
624
+
575
625
  def build_error(status, body, retry_after = nil)
576
626
  parsed = begin
577
627
  JSON.parse(body)
@@ -579,7 +629,7 @@ module ParseAPI
579
629
  {}
580
630
  end
581
631
  parsed = {} unless parsed.is_a?(Hash)
582
- Error.new(
632
+ error = Error.new(
583
633
  status: status,
584
634
  code: parsed['code'].is_a?(String) ? parsed['code'] : 'unknown_error',
585
635
  message: parsed['message'].is_a?(String) ? parsed['message'] : "Request failed with status #{status}",
@@ -587,6 +637,19 @@ module ParseAPI
587
637
  request_id: parsed['request_id'].is_a?(String) ? parsed['request_id'] : nil,
588
638
  retry_after: retry_after
589
639
  )
640
+ if status == 409 && error.code == 'ambiguous_city' && parsed['candidates'].is_a?(Array)
641
+ candidates = parsed['candidates'].first(2).map do |row|
642
+ next unless row.is_a?(Hash) && row['id'].is_a?(String) && /\Acity_[a-z0-9]{12}\z/.match?(row['id']) && row['object'] == 'city'
643
+ candidate = { 'id' => row['id'], 'object' => 'city' }
644
+ %w[name name_local country country_name state state_name type].each do |field|
645
+ value = row[field]
646
+ candidate[field] = value if row.key?(field) && (value.nil? || (value.is_a?(String) && value.length <= 255))
647
+ end
648
+ candidate
649
+ end.compact
650
+ error.instance_variable_set(:@candidates, candidates)
651
+ end
652
+ error
590
653
  end
591
654
  end
592
655
  end
@@ -1,3 +1,3 @@
1
1
  module ParseAPI
2
- VERSION = '1.9.2'.freeze
2
+ VERSION = '1.10.0'.freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: parseapi
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.2
4
+ version: 1.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ParseAPI
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-14 00:00:00.000000000 Z
11
+ date: 2026-10-16 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email: