A library that makes it easy to query the Overpass API and translate its responses into RGeo objects. It supports queries written in the Overpass QL.
Install globally:
gem install underpass
Or put it in your Gemfile:
gem 'underpass'
require 'underpass'
# Define a bounding box polygon
wkt = <<-WKT
POLYGON ((
23.669 47.65,
23.725 47.65,
23.725 47.674,
23.669 47.674,
23.669 47.65
))
WKT
bbox = RGeo::Geographic.spherical_factory.parse_wkt(wkt)
# Query using raw Overpass QL
query = 'way["heritage:operator"="lmi"]["heritage"="2"];'
features = Underpass::QL::Query.perform(bbox, query)
# Each result is a Feature with geometry and OSM tags
features.each do |f|
puts f.geometry.as_text # => "POLYGON ((...)"
puts f.properties[:name] # => "Biserica Romano-Catolică"
puts f.id # => 186213580
puts f.type # => "way"
endSee more usage examples.
For comprehensive examples with real data covering all return types and functionality, see the usage-examples.md file which includes examples for:
- Node queries (Point geometries) - restaurants, cafes, etc.
- Way queries (LineString/Polygon geometries) - roads, buildings, parks
- Relation queries (MultiPolygon/MultiLineString geometries) - lakes, bus routes
- Area queries using
perform_in_area - Raw queries using
perform_rawwith inline bounding boxes - Around queries for proximity search
- Builder DSL for constructing queries
- Post-query filtering
- GeoJSON export
All query results are returned as Underpass::Feature objects that pair an RGeo
geometry with OpenStreetMap metadata:
feature.geometry # RGeo geometry (Point, LineString, Polygon, Multi*)
feature.properties # Hash of OSM tags, e.g. { name: "...", amenity: "..." }
feature.id # OSM element ID (Integer)
feature.type # "node", "way", or "relation"Instead of writing raw Overpass QL strings, you can use the chainable Ruby DSL:
# Simple query
query = Underpass::QL::Builder.new
.node(amenity: 'restaurant')
.to_ql
# => 'node["amenity"="restaurant"];'
# Multiple types
query = Underpass::QL::Builder.new
.node(amenity: 'restaurant')
.way(highway: 'primary')
.to_ql
# => "node[\"amenity\"=\"restaurant\"];\nway[\"highway\"=\"primary\"];"
# Multiple tag filters
query = Underpass::QL::Builder.new
.way('heritage:operator': 'lmi', heritage: '2')
.to_ql
# => 'way["heritage:operator"="lmi"]["heritage"="2"];'
# nwr (node/way/relation) shorthand
query = Underpass::QL::Builder.new
.nwr(name: 'Central Park')
.to_ql
# => 'nwr["name"="Central Park"];'
# Pass a Builder directly to Query.perform
builder = Underpass::QL::Builder.new.way(building: 'yes')
features = Underpass::QL::Query.perform(bbox, builder)The Builder DSL is designed to work with Query.perform. Define a bounding box
as an RGeo geometry, build your query with the DSL, and pass both to perform:
# Define a bounding box
wkt = 'POLYGON ((26.08 44.42, 26.12 44.42, 26.12 44.45, 26.08 44.45, 26.08 44.42))'
bbox = RGeo::Geographic.spherical_factory.parse_wkt(wkt)
# Build the query
builder = Underpass::QL::Builder.new.node(amenity: 'cafe')
# Execute — the bounding box constrains results spatially
cafes = Underpass::QL::Query.perform(bbox, builder)Note: The Builder DSL generates the Overpass QL query body (e.g. node["amenity"="cafe"];),
while the bounding box is applied as a separate spatial constraint by Query.perform.
You can inspect the generated query with builder.to_ql.
Find elements within a radius (in meters) of a point:
# Using coordinates
query = Underpass::QL::Builder.new
.node(amenity: 'restaurant')
.around(500, 47.65, 23.69)
.to_ql
# => 'node["amenity"="restaurant"](around:500,47.65,23.69);'
# Using an RGeo point
point = RGeo::Geographic.spherical_factory(srid: 4326).point(23.69, 47.65)
query = Underpass::QL::Builder.new
.node(amenity: 'cafe')
.around(1000, point)
.to_qlThe around filter is appended to all statements in the builder.
Query within a named geographic area instead of a bounding box:
features = Underpass::QL::Query.perform_in_area(
'Romania',
'node["amenity"="restaurant"];'
)This generates an Overpass query using the area statement:
[out:json][timeout:25];
area["name"="Romania"]->.searchArea;
(
node["amenity"="restaurant"](area.searchArea);
);
out body;
>;
out skel qt;
Builder objects work with perform_in_area as well:
builder = Underpass::QL::Builder.new.node(amenity: 'restaurant')
features = Underpass::QL::Query.perform_in_area('Romania', builder)When your query already contains its own bounding box (or other spatial filters)
inline, use perform_raw to skip the global bounding box:
query = 'node["name"="Peak"]["natural"="peak"](47.0,25.0,47.1,25.1);'
features = Underpass::QL::Query.perform_raw(query)The query body is wrapped in the standard Overpass request template (output format,
timeout, recurse) but no global bounding box is added. This is useful when you
construct queries with element-level bounding boxes or other spatial constraints
that don't fit the perform / perform_in_area patterns.
Relations tagged type=multipolygon are automatically assembled into proper RGeo
polygons with holes. Outer member ways are chained into exterior rings, inner member
ways become interior rings (holes). Multiple outer rings produce a MultiPolygon.
query = 'relation["type"="multipolygon"]["name"="Some Lake"];'
features = Underpass::QL::Query.perform(bbox, query)
feature = features.first
feature.geometry
# => RGeo::Geographic::SphericalPolygonImpl (with interior rings for islands)Relations tagged type=route (bus lines, hiking trails, etc.) are assembled into
MultiLineString geometries:
query = 'relation["type"="route"]["name"="Bus 42"];'
features = Underpass::QL::Query.perform(bbox, query)
feature = features.first
feature.geometry
# => RGeo::Geographic::SphericalMultiLineStringImplRelations without a recognized type tag are expanded into individual member geometries (the previous behavior), with each member geometry wrapped in a Feature carrying the parent relation's tags.
Convert results to GeoJSON for use with web mapping libraries:
features = Underpass::QL::Query.perform(bbox, query)
geojson = Underpass::GeoJSON.encode(features)
# geojson is a Hash:
# {
# "type" => "FeatureCollection",
# "features" => [
# {
# "type" => "Feature",
# "geometry" => { "type" => "Point", "coordinates" => [23.69, 47.65] },
# "properties" => { "name" => "...", "amenity" => "restaurant" },
# "id" => 123456
# },
# ...
# ]
# }
# Serialize to JSON
require 'json'
File.write('output.geojson', JSON.pretty_generate(geojson))This requires the rgeo-geojson gem, which is included as a dependency.
Filter results by tag properties after querying, without modifying the Overpass query:
features = Underpass::QL::Query.perform(bbox, 'nwr["amenity"];')
# Exact match
restaurants = Underpass::Filter.new(features).where(amenity: 'restaurant')
# Regex match
italian = Underpass::Filter.new(features).where(cuisine: /italian/i)
# Multiple acceptable values (OR)
food = Underpass::Filter.new(features).where(amenity: %w[restaurant cafe bar])
# Multiple conditions (AND)
chinese_restaurants = Underpass::Filter.new(features).where(
amenity: 'restaurant',
cuisine: 'chinese'
)
# Rejection
no_banks = Underpass::Filter.new(features).reject(amenity: 'bank')For large result sets, use lazy_matches to avoid building the entire array in memory:
matcher = Underpass::Matcher.new(response, requested_types)
# Process results lazily
matcher.lazy_matches.each do |feature|
# Each Feature is created on demand
puts feature.properties[:name]
end
# Take only the first 10
first_ten = matcher.lazy_matches.first(10)
# Chain lazy operations
matcher.lazy_matches
.select { |f| f.properties[:amenity] == 'restaurant' }
.map(&:geometry)
.first(5)Matcher#matches is implemented in terms of lazy_matches.to_a, so eager
evaluation still works identically.
The library includes a query analyzer that automatically determines which types of matches (node, way, or relation) you're interested in based on your query. This ensures that only the requested match types are returned.
- The query is trimmed and split on semicolons (
;) - For each line, the analyzer looks at the first word
- If the first word is
node,way, orrelation, that type is added to the requested match types - The library returns only matches of the requested types that have the
tagskey
Query for ways only:
query = 'way["highway"="primary"];'
features = Underpass::QL::Query.perform(bbox, query)
# Returns only way matchesQuery for nodes and relations:
query = 'node["amenity"="restaurant"]; relation["type"="multipolygon"];'
features = Underpass::QL::Query.perform(bbox, query)
# Returns node and relation matches, but no way matchesQuery with unrecognized type (returns all types):
query = 'nwr["name"="Example"];' # nwr is not a specific type
features = Underpass::QL::Query.perform(bbox, query)
# Returns all match types (node, way, and relation)Point to a private Overpass instance instead of the public one:
Underpass.configure do |c|
c.api_endpoint = 'https://my-overpass.example.com/api/interpreter'
endChange the Overpass query timeout (default: 25 seconds):
Underpass.configure do |c|
c.timeout = 60
endChange the maximum number of retry attempts for rate limiting and timeout errors (default: 3):
Underpass.configure do |c|
c.max_retries = 5
endUnderpass.reset_configuration!The client automatically retries on transient errors with exponential backoff:
- HTTP 429 (rate limited) -- retries up to
max_retriestimes (default: 3), then raisesUnderpass::RateLimitError - HTTP 504 (gateway timeout) -- retries up to
max_retriestimes (default: 3), then raisesUnderpass::TimeoutError - Other errors -- raises
Underpass::ApiErrorimmediately
All errors inherit from Underpass::Error, which inherits from StandardError.
Errors include structured data parsed from Overpass API responses:
begin
features = Underpass::QL::Query.perform(bbox, query)
rescue Underpass::TimeoutError => e
e.code # => "timeout"
e.error_message # => "Query timed out in \"query\" at line 3 after 25 seconds."
e.details # => { line: 3, timeout_seconds: 25 }
e.http_status # => 504
# Convert to hash or JSON for logging/APIs
e.to_h # => { code: "timeout", message: "...", details: {...} }
e.to_json # => JSON string
rescue Underpass::RateLimitError
puts "Rate limited by the Overpass API, try again later"
rescue Underpass::ApiError => e
puts "API error (#{e.code}): #{e.error_message}"
end| Code | Description | HTTP Status |
|---|---|---|
timeout |
Query exceeded time limit | 504 |
memory |
Query exceeded memory limit | 504 |
rate_limit |
Too many requests | 429 |
syntax |
Query syntax error | 400 |
runtime |
Other runtime errors | varies |
unknown |
Unparseable error | varies |
Enable in-memory caching to avoid redundant API calls during development:
# Enable with a 10-minute TTL
Underpass.cache = Underpass::Cache.new(ttl: 600)
# Subsequent identical queries return cached responses
features = Underpass::QL::Query.perform(bbox, query) # hits API
features = Underpass::QL::Query.perform(bbox, query) # returns cached
# Clear the cache
Underpass.cache.clear
# Disable caching
Underpass.cache = nilCaching is disabled by default. Cache keys are SHA-256 digests of the full query string, so different queries always produce different keys.
The Overpass recurse operator can be configured per request. The default (>)
fetches child elements, which is needed to resolve way nodes:
# Default: child recurse (>)
request = Underpass::QL::Request.new(query, bbox)
# Descendant recurse (>>)
request = Underpass::QL::Request.new(query, bbox, recurse: '>>')
# Parent recurse (<)
request = Underpass::QL::Request.new(query, bbox, recurse: '<')
# No recurse
request = Underpass::QL::Request.new(query, bbox, recurse: nil)Have a look at the issue tracker.
For detailed, working examples with real data that cover all return types and functionality of the library, see the usage-examples.md file. These examples demonstrate:
- Node queries (Point geometries) - restaurants, cafes, bus stops
- Way queries (LineString geometries) - primary roads, highways
- Way queries (Polygon geometries) - buildings, parks
- Relation queries (MultiPolygon geometries) - lakes with islands
- Relation queries (MultiLineString geometries) - bus routes, hiking trails
- Area queries - using
perform_in_areainstead of bounding boxes - Around queries - proximity search within a radius
- Builder DSL - constructing queries programmatically
- Post-query filtering - filtering results by properties
- GeoJSON export - converting results for web mapping libraries
All examples use real data from OpenStreetMap and have been tested to work correctly.
- Check out the latest master branch to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
- Check out the issue tracker to make sure someone already hasn't requested it and / or contributed it
- Fork the project, clone the fork, run
bundle installand then make surerspecruns - Start a feature / bugfix branch
- Commit and push until you are happy with your contribution
- Make sure to add specs for it - this is important so your contribution won't be broken in a future version unintentionally
- Open a pull request
Further tips:
- To test drive the library run
bundle console
Things to read if you want to get familiar with RGeo
- RGeo RDoc
- Geo-Rails Part 1: A Call to Revolution
- Geo-Rails Part 2: Setting Up a Geospatial Rails App
- Geo-Rails Part 3: Spatial Data Types with RGeo
- Geo-Rails Part 4: Coordinate Systems and Projections
- Geo-Rails Part 5: Spatial Data Formats
- Geo-Rails Part 6: Scaling Spatial Applications
- Geo-Rails Part 7: Geometry vs. Geography, or, How I Learned To Stop Worrying And Love Projections
- Geo-Rails Part 8: ZCTA Lookup, A Worked Example
- Geo-Rails Part 9: The PostGIS spatial_ref_sys Table and You
underpass is released under the MIT License. See the LICENSE file for further details.