underpass 0.0.7 → 0.9.1

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.
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Underpass
4
+ # Stores configuration options for the Underpass library.
5
+ #
6
+ # @example
7
+ # Underpass.configure do |config|
8
+ # config.api_endpoint = 'https://overpass.kumi.systems/api/interpreter'
9
+ # config.timeout = 30
10
+ # config.max_retries = 5
11
+ # end
12
+ class Configuration
13
+ # @return [String] the Overpass API endpoint URL
14
+ attr_accessor :api_endpoint
15
+
16
+ # @return [Integer] the query timeout in seconds
17
+ attr_accessor :timeout
18
+
19
+ # @return [Integer] maximum number of retry attempts for rate limiting and timeouts
20
+ attr_accessor :max_retries
21
+
22
+ def initialize
23
+ @api_endpoint = 'https://overpass-api.de/api/interpreter'
24
+ @timeout = 25
25
+ @max_retries = 3
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Underpass
4
+ # Parses HTML error responses from the Overpass API into structured data.
5
+ #
6
+ # The Overpass API returns HTML error pages when queries fail. This class
7
+ # extracts error information from those responses and returns structured
8
+ # hashes with code, message, and details.
9
+ #
10
+ # @example
11
+ # result = ErrorParser.parse("<html>...runtime error: Query timed out...</html>", 504)
12
+ # result[:code] # => "timeout"
13
+ # result[:message] # => "Query timed out in \"query\" at line 3 after 25 seconds."
14
+ # result[:details] # => { line: 3, timeout_seconds: 25 }
15
+ class ErrorParser
16
+ # Known error patterns from Overpass API responses
17
+ PATTERNS = {
18
+ timeout: /Query timed out.*?at line (\d+) after (\d+) seconds/i,
19
+ memory: /Query run out of memory.*?(\d+)\s*MB/i,
20
+ syntax: /parse error:?\s*(.+)/i,
21
+ runtime: /runtime error:?\s*(.+)/i
22
+ }.freeze
23
+
24
+ # Parses an error response body and returns structured error data.
25
+ #
26
+ # @param response_body [String] the raw response body (usually HTML)
27
+ # @param status_code [Integer] the HTTP status code
28
+ # @return [Hash] structured error data with :code, :message, and :details keys
29
+ def self.parse(response_body, status_code)
30
+ return rate_limit_result if status_code == 429
31
+
32
+ text = extract_error_text(response_body)
33
+ parse_error_text(text, status_code)
34
+ end
35
+
36
+ # Extracts error text from HTML or returns the body as-is.
37
+ #
38
+ # @param body [String] the response body
39
+ # @return [String] the extracted error text
40
+ def self.extract_error_text(body)
41
+ return '' if body.nil? || body.empty?
42
+
43
+ # Try to extract text from <strong> tags (common Overpass error format)
44
+ if (match = body.match(%r{<strong[^>]*>(.*?)</strong>}im))
45
+ return match[1].gsub(/<[^>]+>/, '').strip
46
+ end
47
+
48
+ # Try to extract from <p> tags
49
+ if (match = body.match(%r{<p[^>]*>(.*?)</p>}im))
50
+ return match[1].gsub(/<[^>]+>/, '').strip
51
+ end
52
+
53
+ # Fall back to stripping all HTML tags
54
+ body.gsub(/<[^>]+>/, ' ').gsub(/\s+/, ' ').strip
55
+ end
56
+ private_class_method :extract_error_text
57
+
58
+ # Parses the error text against known patterns.
59
+ #
60
+ # @param text [String] the extracted error text
61
+ # @param status_code [Integer] the HTTP status code
62
+ # @return [Hash] structured error data
63
+ def self.parse_error_text(text, status_code)
64
+ parse_timeout(text) ||
65
+ parse_memory(text) ||
66
+ parse_syntax(text) ||
67
+ parse_runtime(text) ||
68
+ unknown_result(text, status_code)
69
+ end
70
+ private_class_method :parse_error_text
71
+
72
+ def self.parse_timeout(text)
73
+ return unless (match = text.match(PATTERNS[:timeout]))
74
+
75
+ { code: 'timeout', message: text, details: { line: match[1].to_i, timeout_seconds: match[2].to_i } }
76
+ end
77
+ private_class_method :parse_timeout
78
+
79
+ def self.parse_memory(text)
80
+ return unless (match = text.match(PATTERNS[:memory]))
81
+
82
+ { code: 'memory', message: text, details: { memory_mb: match[1].to_i } }
83
+ end
84
+ private_class_method :parse_memory
85
+
86
+ def self.parse_syntax(text)
87
+ return unless (match = text.match(PATTERNS[:syntax]))
88
+
89
+ { code: 'syntax', message: match[1].strip, details: extract_syntax_details(match[1]) }
90
+ end
91
+ private_class_method :parse_syntax
92
+
93
+ def self.parse_runtime(text)
94
+ return unless (match = text.match(PATTERNS[:runtime]))
95
+
96
+ { code: 'runtime', message: match[1].strip, details: {} }
97
+ end
98
+ private_class_method :parse_runtime
99
+
100
+ # Extracts line number from syntax error messages if present.
101
+ #
102
+ # @param message [String] the error message
103
+ # @return [Hash] details hash with line number if found
104
+ def self.extract_syntax_details(message)
105
+ if (match = message.match(/line\s+(\d+)/i))
106
+ { line: match[1].to_i }
107
+ else
108
+ {}
109
+ end
110
+ end
111
+ private_class_method :extract_syntax_details
112
+
113
+ # Returns a rate limit error result.
114
+ #
115
+ # @return [Hash] structured error data for rate limiting
116
+ def self.rate_limit_result
117
+ {
118
+ code: 'rate_limit',
119
+ message: 'Rate limited by the Overpass API',
120
+ details: {}
121
+ }
122
+ end
123
+ private_class_method :rate_limit_result
124
+
125
+ # Returns an unknown error result.
126
+ #
127
+ # @param text [String] the error text
128
+ # @param status_code [Integer] the HTTP status code
129
+ # @return [Hash] structured error data for unknown errors
130
+ def self.unknown_result(text, status_code)
131
+ message = text.empty? ? "HTTP #{status_code} error" : text
132
+ {
133
+ code: 'unknown',
134
+ message: message,
135
+ details: {}
136
+ }
137
+ end
138
+ private_class_method :unknown_result
139
+ end
140
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Underpass
6
+ # Base error class for all Underpass errors.
7
+ #
8
+ # Provides structured error data parsed from Overpass API responses.
9
+ #
10
+ # @example
11
+ # begin
12
+ # features = Underpass::QL::Query.perform(bbox, query)
13
+ # rescue Underpass::Error => e
14
+ # e.code # => "timeout"
15
+ # e.error_message # => "Query timed out..."
16
+ # e.details # => { line: 3, timeout_seconds: 25 }
17
+ # e.http_status # => 504
18
+ # e.to_h # => { code: "timeout", message: "...", details: {...} }
19
+ # end
20
+ class Error < StandardError
21
+ # @return [String, nil] the error code (e.g., "timeout", "memory", "syntax")
22
+ attr_reader :code
23
+
24
+ # @return [String, nil] the human-readable error message
25
+ attr_reader :error_message
26
+
27
+ # @return [Hash] additional error details (varies by error type)
28
+ attr_reader :details
29
+
30
+ # @return [Integer, nil] the HTTP status code from the API response
31
+ attr_reader :http_status
32
+
33
+ # Creates a new error with optional structured data.
34
+ #
35
+ # @param message [String, nil] the error message (used by StandardError)
36
+ # @param code [String, nil] the error code
37
+ # @param error_message [String, nil] the detailed error message
38
+ # @param details [Hash] additional error details
39
+ # @param http_status [Integer, nil] the HTTP status code
40
+ def initialize(message = nil, code: nil, error_message: nil, details: {}, http_status: nil)
41
+ @code = code
42
+ @error_message = error_message || message
43
+ @details = details || {}
44
+ @http_status = http_status
45
+ super(@error_message || message)
46
+ end
47
+
48
+ # Returns a hash representation of the error.
49
+ #
50
+ # @return [Hash] the error as a hash with :code, :message, and :details keys
51
+ def to_h
52
+ {
53
+ code: code,
54
+ message: error_message,
55
+ details: details
56
+ }
57
+ end
58
+
59
+ # Returns a JSON representation of the error.
60
+ #
61
+ # @param args [Array] arguments passed to JSON.generate
62
+ # @return [String] the error as a JSON string
63
+ def to_json(*)
64
+ to_h.to_json(*)
65
+ end
66
+ end
67
+
68
+ # Raised when the Overpass API returns a 429 rate limit response.
69
+ class RateLimitError < Error; end
70
+
71
+ # Raised when the Overpass API returns a 504 timeout response.
72
+ class TimeoutError < Error; end
73
+
74
+ # Raised when the Overpass API returns an unexpected error response.
75
+ class ApiError < Error; end
76
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Underpass
4
+ # Wraps an RGeo geometry with OSM metadata (tags, id, type).
5
+ #
6
+ # Returned by {QL::Query.perform} and {Matcher#matches} for each matched
7
+ # element in the Overpass API response.
8
+ class Feature
9
+ # @return [RGeo::Feature::Geometry] the RGeo geometry object
10
+ attr_reader :geometry
11
+
12
+ # @return [Hash{Symbol => String}] the OSM tags for this element
13
+ attr_reader :properties
14
+
15
+ # @return [Integer, nil] the OSM element ID
16
+ attr_reader :id
17
+
18
+ # @return [String, nil] the OSM element type ("node", "way", or "relation")
19
+ attr_reader :type
20
+
21
+ # Creates a new Feature.
22
+ #
23
+ # @param geometry [RGeo::Feature::Geometry] the RGeo geometry
24
+ # @param properties [Hash] the OSM tags
25
+ # @param id [Integer, nil] the OSM element ID
26
+ # @param type [String, nil] the OSM element type
27
+ def initialize(geometry:, properties: {}, id: nil, type: nil)
28
+ @geometry = geometry
29
+ @properties = properties
30
+ @id = id
31
+ @type = type
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Underpass
4
+ # Post-query filtering of {Feature} objects by tag properties.
5
+ #
6
+ # @example Filter restaurants by cuisine
7
+ # filter = Underpass::Filter.new(features)
8
+ # italian = filter.where(cuisine: 'italian')
9
+ class Filter
10
+ # Creates a new filter for the given features.
11
+ #
12
+ # @param features [Array<Feature>] the features to filter
13
+ def initialize(features)
14
+ @features = features
15
+ end
16
+
17
+ # Returns features whose properties match all given conditions.
18
+ #
19
+ # Conditions can be exact values, regular expressions, or arrays of values.
20
+ #
21
+ # @param conditions [Hash{Symbol => String, Regexp, Array}] tag conditions to match
22
+ # @return [Array<Feature>] features matching all conditions
23
+ #
24
+ # @example Exact match
25
+ # filter.where(cuisine: 'italian')
26
+ # @example Regex match
27
+ # filter.where(name: /pizza/i)
28
+ # @example Array inclusion
29
+ # filter.where(cuisine: ['italian', 'mexican'])
30
+ def where(conditions = {})
31
+ @features.select do |feature|
32
+ conditions.all? { |key, value| match_condition?(feature.properties[key], value) }
33
+ end
34
+ end
35
+
36
+ # Returns features that do not match any of the given conditions.
37
+ #
38
+ # @param conditions [Hash{Symbol => String}] tag conditions to reject
39
+ # @return [Array<Feature>] features not matching any condition
40
+ def reject(conditions = {})
41
+ @features.reject do |feature|
42
+ conditions.any? { |key, value| feature.properties[key] == value.to_s }
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def match_condition?(prop_value, condition)
49
+ case condition
50
+ when Regexp then prop_value&.match?(condition)
51
+ when Array then condition.include?(prop_value)
52
+ else prop_value == condition.to_s
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rgeo/geo_json'
4
+
5
+ module Underpass
6
+ # Encodes {Feature} arrays as GeoJSON FeatureCollections.
7
+ #
8
+ # @example Export query results to GeoJSON
9
+ # features = Underpass::QL::Query.perform(bbox, query)
10
+ # geojson = Underpass::GeoJSON.encode(features)
11
+ module GeoJSON
12
+ # Encodes an array of features as a GeoJSON FeatureCollection hash.
13
+ #
14
+ # @param features [Array<Feature>] the features to encode
15
+ # @return [Hash] a GeoJSON FeatureCollection
16
+ def self.encode(features)
17
+ geo_features = features.map do |f|
18
+ RGeo::GeoJSON::Feature.new(f.geometry, f.id, f.properties)
19
+ end
20
+ RGeo::GeoJSON.encode(RGeo::GeoJSON::FeatureCollection.new(geo_features))
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Underpass
4
+ # Extracts matching elements from an Overpass API response.
5
+ #
6
+ # A "match" is a response element that has a +tags+ key, indicating it is
7
+ # a tagged OSM element rather than a bare geometry node. Each match is
8
+ # converted into a {Feature} with the appropriate RGeo geometry.
9
+ class Matcher
10
+ # Creates a new matcher for the given response.
11
+ #
12
+ # @param response [QL::Response] a parsed API response
13
+ # @param requested_types [Array<String>, nil] element types to include
14
+ # (e.g. +["node", "way"]+). Defaults to all types when +nil+.
15
+ def initialize(response, requested_types = nil)
16
+ @nodes = response.nodes
17
+ @ways = response.ways
18
+ @relations = response.relations
19
+ @requested_types = requested_types || %w[node way relation]
20
+ end
21
+
22
+ # Returns all matched features as an array.
23
+ #
24
+ # @return [Array<Feature>] the matched features
25
+ def matches
26
+ @matches ||= lazy_matches.to_a
27
+ end
28
+
29
+ # Returns a lazy enumerator of matched features.
30
+ #
31
+ # @return [Enumerator::Lazy<Feature>] lazy enumerator of features
32
+ def lazy_matches
33
+ tagged_elements.lazy.flat_map { |element| features_for(element) }
34
+ end
35
+
36
+ private
37
+
38
+ def tagged_elements
39
+ elements = []
40
+ elements.concat(@nodes.values.select { |n| n.key?(:tags) }) if @requested_types.include?('node')
41
+ elements.concat(@ways.values.select { |w| w.key?(:tags) }) if @requested_types.include?('way')
42
+ elements.concat(@relations.values.select { |r| r.key?(:tags) }) if @requested_types.include?('relation')
43
+ elements
44
+ end
45
+
46
+ def features_for(element)
47
+ case element[:type]
48
+ when 'node'
49
+ [build_feature(Shape.point_from_node(element), element)]
50
+ when 'way'
51
+ [build_feature(way_geometry(element), element)]
52
+ when 'relation'
53
+ relation_features(element)
54
+ else
55
+ []
56
+ end
57
+ end
58
+
59
+ def relation_features(relation)
60
+ geometry = relation_geometry(relation)
61
+ if geometry.is_a?(Array)
62
+ geometry.map { |g| build_feature(g, relation) }
63
+ else
64
+ [build_feature(geometry, relation)]
65
+ end
66
+ end
67
+
68
+ def relation_geometry(relation)
69
+ case relation[:tags][:type]
70
+ when 'multipolygon'
71
+ Shape.multipolygon_from_relation(relation, @ways, @nodes)
72
+ when 'route'
73
+ Shape.multi_line_string_from_relation(relation, @ways, @nodes)
74
+ else
75
+ expand_relation_members(relation)
76
+ end
77
+ end
78
+
79
+ def expand_relation_members(relation)
80
+ relation[:members].filter_map do |member|
81
+ case member[:type]
82
+ when 'node' then Shape.point_from_node(@nodes[member[:ref]])
83
+ when 'way' then way_geometry(@ways[member[:ref]])
84
+ end
85
+ end
86
+ end
87
+
88
+ def way_geometry(way)
89
+ if Shape.open_way?(way)
90
+ Shape.polygon_from_way(way, @nodes)
91
+ else
92
+ Shape.line_string_from_way(way, @nodes)
93
+ end
94
+ end
95
+
96
+ def build_feature(geometry, element)
97
+ Feature.new(
98
+ geometry: geometry,
99
+ properties: element[:tags] || {},
100
+ id: element[:id],
101
+ type: element[:type]
102
+ )
103
+ end
104
+ end
105
+ end
@@ -2,13 +2,26 @@
2
2
 
3
3
  module Underpass
4
4
  module QL
5
- # Bounding box related utilities.
5
+ # Converts RGeo geometries and WKT strings into Overpass QL bounding box syntax.
6
6
  class BoundingBox
7
- # Returns the Overpass query language bounding box string
8
- # when provided with an RGeo geometry
9
- def self.from_geometry(geometry)
10
- r_bb = RGeo::Cartesian::BoundingBox.create_from_geometry(geometry)
11
- "bbox:#{r_bb.min_y},#{r_bb.min_x},#{r_bb.max_y},#{r_bb.max_x}"
7
+ class << self
8
+ # Returns the Overpass QL bounding box string from a WKT string.
9
+ #
10
+ # @param wkt [String] a Well Known Text geometry string
11
+ # @return [String] an Overpass QL bounding box (e.g. +"bbox:47.65,23.669,47.674,23.725"+)
12
+ def from_wkt(wkt)
13
+ geometry = RGeo::Geographic.spherical_factory.parse_wkt(wkt)
14
+ from_geometry(geometry)
15
+ end
16
+
17
+ # Returns the Overpass QL bounding box string from an RGeo geometry.
18
+ #
19
+ # @param geometry [RGeo::Feature::Geometry] an RGeo geometry
20
+ # @return [String] an Overpass QL bounding box (e.g. +"bbox:47.65,23.669,47.674,23.725"+)
21
+ def from_geometry(geometry)
22
+ r_bb = RGeo::Cartesian::BoundingBox.create_from_geometry(geometry)
23
+ "bbox:#{r_bb.min_y},#{r_bb.min_x},#{r_bb.max_y},#{r_bb.max_x}"
24
+ end
12
25
  end
13
26
  end
14
27
  end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Underpass
4
+ module QL
5
+ # DSL for building Overpass QL queries programmatically.
6
+ #
7
+ # @example Query for restaurants
8
+ # builder = Underpass::QL::Builder.new
9
+ # builder.node('amenity' => 'restaurant')
10
+ # Underpass::QL::Query.perform(bbox, builder)
11
+ #
12
+ # @example Proximity search with around
13
+ # builder = Underpass::QL::Builder.new
14
+ # builder.node('amenity' => 'cafe').around(500, 44.4268, 26.1025)
15
+ class Builder
16
+ # Creates a new empty builder.
17
+ def initialize
18
+ @statements = []
19
+ @around = nil
20
+ end
21
+
22
+ # Adds a node query statement.
23
+ #
24
+ # @param tags [Hash{String => String}] tag filters
25
+ # @return [self] for method chaining
26
+ def node(tags = {})
27
+ @statements << build_statement('node', tags)
28
+ self
29
+ end
30
+
31
+ # Adds a way query statement.
32
+ #
33
+ # @param tags [Hash{String => String}] tag filters
34
+ # @return [self] for method chaining
35
+ def way(tags = {})
36
+ @statements << build_statement('way', tags)
37
+ self
38
+ end
39
+
40
+ # Adds a relation query statement.
41
+ #
42
+ # @param tags [Hash{String => String}] tag filters
43
+ # @return [self] for method chaining
44
+ def relation(tags = {})
45
+ @statements << build_statement('relation', tags)
46
+ self
47
+ end
48
+
49
+ # Adds a node/way/relation (nwr) query statement.
50
+ #
51
+ # @param tags [Hash{String => String}] tag filters
52
+ # @return [self] for method chaining
53
+ def nwr(tags = {})
54
+ @statements << build_statement('nwr', tags)
55
+ self
56
+ end
57
+
58
+ # Sets a proximity filter for all statements.
59
+ #
60
+ # @param radius [Numeric] search radius in meters
61
+ # @param lat_or_point [Numeric, RGeo::Feature::Point] latitude or an RGeo point
62
+ # @param lon [Numeric, nil] longitude (required when +lat_or_point+ is numeric)
63
+ # @return [self] for method chaining
64
+ def around(radius, lat_or_point, lon = nil)
65
+ @around = if lat_or_point.respond_to?(:y)
66
+ { radius: radius, lat: lat_or_point.y, lon: lat_or_point.x }
67
+ else
68
+ { radius: radius, lat: lat_or_point, lon: lon }
69
+ end
70
+ self
71
+ end
72
+
73
+ # Converts the builder into an Overpass QL query string.
74
+ #
75
+ # @return [String] the Overpass QL query
76
+ def to_ql
77
+ if @around
78
+ @statements.map { |s| append_around(s) }.join("\n")
79
+ else
80
+ @statements.join("\n")
81
+ end
82
+ end
83
+
84
+ private
85
+
86
+ def build_statement(type, tags)
87
+ tag_filters = tags.map { |k, v| "[\"#{k}\"=\"#{v}\"]" }.join
88
+ "#{type}#{tag_filters};"
89
+ end
90
+
91
+ def append_around(statement)
92
+ around_filter = "(around:#{@around[:radius]},#{@around[:lat]},#{@around[:lon]})"
93
+ statement.sub(';', "#{around_filter};")
94
+ end
95
+ end
96
+ end
97
+ end
@@ -1,17 +1,82 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Underpass
4
+ # Namespace for Overpass Query Language related classes.
4
5
  module QL
5
- # Provides a shortcut method that makes it easy to work with the library
6
+ # High-level entry point for querying the Overpass API.
7
+ #
8
+ # Glues together {Request}, {Client}, {Response}, {QueryAnalyzer}, and
9
+ # {Matcher} to provide a single-call interface.
10
+ #
11
+ # @example Query with a bounding box
12
+ # features = Underpass::QL::Query.perform(bbox, 'way["building"="yes"];')
13
+ #
14
+ # @example Query with a named area
15
+ # features = Underpass::QL::Query.perform_in_area('Romania', 'node["place"="city"];')
16
+ #
17
+ # @example Raw query with inline bounding box
18
+ # query = 'node["name"="Peak"]["natural"="peak"](47.0,25.0,47.1,25.1);'
19
+ # features = Underpass::QL::Query.perform_raw(query)
6
20
  class Query
7
- # Shortcut method that glues together the whole library.
8
- # * +bounding_box+ an RGeo polygon
9
- # * +query+ is the Overpass QL query
21
+ # Queries the Overpass API within a bounding box.
22
+ #
23
+ # @param bounding_box [RGeo::Feature::Geometry] an RGeo polygon defining the search area
24
+ # @param query [String, Builder] an Overpass QL query string or a {Builder} instance
25
+ # @return [Array<Feature>] the matched features
26
+ # @raise [RateLimitError] when rate limited after exhausting retries
27
+ # @raise [TimeoutError] when the API times out after exhausting retries
28
+ # @raise [ApiError] when the API returns an unexpected error
10
29
  def self.perform(bounding_box, query)
11
- op_bbox = Underpass::QL::BoundingBox.from_geometry(bounding_box)
12
- response = Underpass::QL::Request.new(query, op_bbox).run
13
- Underpass::QL::Parser.new(response).parse.matches
30
+ query_string = resolve_query(query)
31
+ op_bbox = Underpass::QL::BoundingBox.from_geometry(bounding_box)
32
+ request = Underpass::QL::Request.new(query_string, op_bbox)
33
+ execute(request, query_string)
14
34
  end
35
+
36
+ # Queries the Overpass API within a named area (e.g. "Romania").
37
+ #
38
+ # @param area_name [String] an OSM area name
39
+ # @param query [String, Builder] an Overpass QL query string or a {Builder} instance
40
+ # @return [Array<Feature>] the matched features
41
+ # @raise [RateLimitError] when rate limited after exhausting retries
42
+ # @raise [TimeoutError] when the API times out after exhausting retries
43
+ # @raise [ApiError] when the API returns an unexpected error
44
+ def self.perform_in_area(area_name, query)
45
+ query_string = resolve_query(query)
46
+ request = Underpass::QL::Request.new(query_string, nil, area_name: area_name)
47
+ execute(request, query_string)
48
+ end
49
+
50
+ # Executes a pre-built query body that includes its own inline bbox.
51
+ # Wraps it in the standard Request template for output format and timeout,
52
+ # without adding a global bounding box.
53
+ #
54
+ # @param query_body [String] Overpass QL body with inline bbox
55
+ # (e.g. +'node["name"="Peak"]["natural"="peak"](47.0,25.0,47.1,25.1);'+)
56
+ # @return [Array<Feature>] the matched features
57
+ # @raise [RateLimitError] when rate limited after exhausting retries
58
+ # @raise [TimeoutError] when the API times out after exhausting retries
59
+ # @raise [ApiError] when the API returns an unexpected error
60
+ def self.perform_raw(query_body)
61
+ request = Underpass::QL::Request.new(query_body, nil)
62
+ execute(request, query_body)
63
+ end
64
+
65
+ def self.resolve_query(query)
66
+ query.respond_to?(:to_ql) ? query.to_ql : query
67
+ end
68
+ private_class_method :resolve_query
69
+
70
+ def self.execute(request, query_string)
71
+ api_response = Underpass::Client.perform(request)
72
+ response = Underpass::QL::Response.new(api_response)
73
+ query_analyzer = Underpass::QL::QueryAnalyzer.new(query_string)
74
+ requested_types = query_analyzer.requested_types
75
+ matcher = Underpass::Matcher.new(response, requested_types)
76
+
77
+ matcher.matches
78
+ end
79
+ private_class_method :execute
15
80
  end
16
81
  end
17
82
  end