From 5d47085e87501c54e6a1f134833342b70470803f Mon Sep 17 00:00:00 2001 From: Michael Agra Date: Tue, 28 Jan 2025 15:37:59 +0000 Subject: [PATCH 1/4] Add support for Ordnance Survey Places API --- README_API_GUIDE.md | 11 ++++ lib/geocoder/lookup.rb | 1 + .../lookups/uk_ordnance_survey_places.rb | 53 ++++++++++++++++++ .../results/uk_ordnance_survey_places.rb | 55 +++++++++++++++++++ .../uk_ordnance_survey_places_sw152qh | 55 +++++++++++++++++++ test/test_helper.rb | 8 +++ test/unit/lookup_test.rb | 2 +- .../unit/lookups/uk_ordnance_survey_places.rb | 17 ++++++ 8 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 lib/geocoder/lookups/uk_ordnance_survey_places.rb create mode 100644 lib/geocoder/results/uk_ordnance_survey_places.rb create mode 100644 test/fixtures/uk_ordnance_survey_places_sw152qh create mode 100644 test/unit/lookups/uk_ordnance_survey_places.rb diff --git a/README_API_GUIDE.md b/README_API_GUIDE.md index 62264ed38..b7e2f9ee1 100644 --- a/README_API_GUIDE.md +++ b/README_API_GUIDE.md @@ -508,6 +508,17 @@ Regional Street Address Lookups * **Terms of Service**: https://developer.ordnancesurvey.co.uk/os-api-framework-agreement * **Limitations**: Only searches postcodes and placenames in England, Wales and Scotland +### Ordnance Survey Places API (`:uk_ordnance_survey_places`) + +* **API key**: required (sign up at https://osdatahub.os.uk) +* **Quota**: 0.01 GBP / transaction (600 transactions-per-minute throttle) +* **Region**: England, Wales, Scotland, Northern Ireland, Channel Islands and Isle of Man +* **SSL support**: yes +* **Languages**: English +* **Documentation**: https://osdatahub.os.uk/docs/places/technicalSpecification +* **Terms of Service**: https://www.ordnancesurvey.co.uk/legal +* **Limitations**: Only searches in England, Wales, Scotland, Northern Ireland, Channel Islands and Isle of Man + ### PostcodeAnywhere UK (`:postcode_anywhere_uk`) * **API key**: required diff --git a/lib/geocoder/lookup.rb b/lib/geocoder/lookup.rb index bef2f3494..a346a1768 100644 --- a/lib/geocoder/lookup.rb +++ b/lib/geocoder/lookup.rb @@ -46,6 +46,7 @@ def street_services :mapbox, :mapquest, :uk_ordnance_survey_names, + :uk_ordnance_survey_places, :opencagedata, :pelias, :pdok_nl, diff --git a/lib/geocoder/lookups/uk_ordnance_survey_places.rb b/lib/geocoder/lookups/uk_ordnance_survey_places.rb new file mode 100644 index 000000000..ceff57eca --- /dev/null +++ b/lib/geocoder/lookups/uk_ordnance_survey_places.rb @@ -0,0 +1,53 @@ +require 'geocoder/lookups/base' +require 'geocoder/results/uk_ordnance_survey_places' + +module Geocoder::Lookup + class UkOrdnanceSurveyPlaces < Base + + def name + 'Ordance Survey Places' + end + + def supported_protocols + [:https] + end + + def base_query_url(query) + "#{protocol}://api.os.uk/search/places/v1/find?" + end + + def required_api_key_parts + ["key"] + end + + def query_url(query) + base_query_url(query) + url_query_string(query) + end + + private # ------------------------------------------------------------- + + def results(query) + return [] unless doc = fetch_data(query) + return [] if doc['header']['totalresults'].zero? + doc['results'].map { |r| r.dig('DPA') || r.dig('LPI') } + end + + def query_url_params(query) + { + query: query.sanitized_text, + key: configuration.api_key, + dataset: 'DPA,LPI', + output_srs: 'EPSG:4326', + fq: filter + }.merge(super) + end + + def country_codes + %w[E W S N L M] + end + + def filter + country_codes.map { |t| "COUNTRY_CODE:#{t}" }.join(' ') + end + end +end diff --git a/lib/geocoder/results/uk_ordnance_survey_places.rb b/lib/geocoder/results/uk_ordnance_survey_places.rb new file mode 100644 index 000000000..6275b288a --- /dev/null +++ b/lib/geocoder/results/uk_ordnance_survey_places.rb @@ -0,0 +1,55 @@ +require 'geocoder/results/base' +require 'easting_northing' + +module Geocoder::Result + class UkOrdnanceSurveyPlaces < Base + + def coordinates + @coordinates ||= Geocoder::EastingNorthing.new( + easting: data['X_COORDINATE'], + northing: data['Y_COORDINATE'], + ).lat_lng + end + + def city + data['POST_TOWN'] + end + + def county + data['DEPENDENT_LOCALITY'] + end + alias state county + + def county_code + '' + end + alias state_code county_code + + def province + { + 'E' => 'England', + 'W' => 'Wales', + 'S' => 'Scotland', + 'N' => 'Northern Ireland', + 'L' => 'Channel Islands', + 'M' => 'Isle of Man', + }[data['COUNTRY_CODE']] + end + + def province_code + data['COUNTRY_CODE'] + end + + def postal_code + data['POSTCODE'] + end + + def country + 'United Kingdom' + end + + def country_code + 'UK' + end + end +end diff --git a/test/fixtures/uk_ordnance_survey_places_sw152qh b/test/fixtures/uk_ordnance_survey_places_sw152qh new file mode 100644 index 000000000..72b2628c0 --- /dev/null +++ b/test/fixtures/uk_ordnance_survey_places_sw152qh @@ -0,0 +1,55 @@ +{ + "header": { + "uri": "https://api.os.uk/search/places/v1/find?query=London%2C%20SW15%202QH&dataset=DPA%2CLPI&output_srs=EPSG%3A4326&maxresults=1", + "query": "query=London, SW15 2QH", + "offset": 0, + "totalresults": 5656265, + "format": "JSON", + "dataset": "DPA,LPI", + "lr": "EN,CY", + "maxresults": 1, + "matchprecision": 1, + "epoch": "115", + "lastupdate": "2025-01-27", + "output_srs": "EPSG:4326" + }, + "results": [ + { + "DPA": { + "UPRN": "100022713096", + "UDPRN": "23996615", + "ADDRESS": "2, WESTROW, LONDON, SW15 6RH", + "BUILDING_NUMBER": "2", + "THOROUGHFARE_NAME": "WESTROW", + "POST_TOWN": "LONDON", + "POSTCODE": "SW15 6RH", + "RPC": "2", + "X_COORDINATE": 523366.0, + "Y_COORDINATE": 174494.0, + "LNG": -0.2257836, + "LAT": 51.4559931, + "STATUS": "APPROVED", + "LOGICAL_STATUS_CODE": "1", + "CLASSIFICATION_CODE": "RD04", + "CLASSIFICATION_CODE_DESCRIPTION": "Terraced", + "LOCAL_CUSTODIAN_CODE": 5960, + "LOCAL_CUSTODIAN_CODE_DESCRIPTION": "WANDSWORTH", + "COUNTRY_CODE": "E", + "COUNTRY_CODE_DESCRIPTION": "This record is within England", + "POSTAL_ADDRESS_CODE": "D", + "POSTAL_ADDRESS_CODE_DESCRIPTION": "A record which is linked to PAF", + "BLPU_STATE_CODE": "2", + "BLPU_STATE_CODE_DESCRIPTION": "In use", + "TOPOGRAPHY_LAYER_TOID": "osgb1000041399472", + "WARD_CODE": "E05014030", + "LAST_UPDATE_DATE": "25/03/2022", + "ENTRY_DATE": "19/03/2001", + "BLPU_STATE_DATE": "11/03/2002", + "LANGUAGE": "EN", + "MATCH": 0.4, + "MATCH_DESCRIPTION": "NO MATCH", + "DELIVERY_POINT_SUFFIX": "1F" + } + } + ] +} \ No newline at end of file diff --git a/test/test_helper.rb b/test/test_helper.rb index cf63d39a2..dd3eeb990 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -450,6 +450,14 @@ def default_fixture_filename end end + require 'geocoder/lookups/uk_ordnance_survey_places' + class Geocoder::Lookup::UkOrdnanceSurveyPlaces + private + def default_fixture_filename + "#{fixture_prefix}_london" + end + end + require 'geocoder/lookups/geoportail_lu' class GeoportailLu private diff --git a/test/unit/lookup_test.rb b/test/unit/lookup_test.rb index ab4dc0b5c..47214f0cf 100644 --- a/test/unit/lookup_test.rb +++ b/test/unit/lookup_test.rb @@ -32,7 +32,7 @@ def test_search_returns_empty_array_when_no_results def test_query_url_contains_values_in_params_hash Geocoder::Lookup.all_services_except_test.each do |l| - next if [:freegeoip, :maxmind_local, :telize, :pointpin, :geoip2, :maxmind_geoip2, :mapbox, :ipdata_co, :ipinfo_io, :ipapi_com, :ipregistry, :ipstack, :postcodes_io, :uk_ordnance_survey_names, :amazon_location_service, :ipbase, :ip2location_lite].include? l # does not use query string + next if [:freegeoip, :maxmind_local, :telize, :pointpin, :geoip2, :maxmind_geoip2, :mapbox, :ipdata_co, :ipinfo_io, :ipapi_com, :ipregistry, :ipstack, :postcodes_io, :uk_ordnance_survey_names, :uk_ordnance_survey_places, :amazon_location_service, :ipbase, :ip2location_lite].include? l # does not use query string set_api_key!(l) url = Geocoder::Lookup.get(l).query_url(Geocoder::Query.new( "test", :params => {:one_in_the_hand => "two in the bush"} diff --git a/test/unit/lookups/uk_ordnance_survey_places.rb b/test/unit/lookups/uk_ordnance_survey_places.rb new file mode 100644 index 000000000..67443d89e --- /dev/null +++ b/test/unit/lookups/uk_ordnance_survey_places.rb @@ -0,0 +1,17 @@ +# encoding: utf-8 +require 'test_helper' + +class UkOrdnanceSurveyPlacesTest < GeocoderTestCase + + def setup + super + Geocoder.configure(lookup: :uk_ordnance_survey_places) + set_api_key!(:uk_ordnance_survey_places) + end + + def test_result_on_postcode_search + result = Geocoder.search('SW152QH').first + assert_in_delta 51.4559931, result.coordinates[0] + assert_in_delta -0.2257836, result.coordinates[1] + end +end From e6bc7a040ba71e45a90409cfe3e197b0a384feee Mon Sep 17 00:00:00 2001 From: Michael Agra Date: Tue, 28 Jan 2025 15:41:18 +0000 Subject: [PATCH 2/4] Add default fixture --- .../fixtures/uk_ordnance_survey_places_london | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 test/fixtures/uk_ordnance_survey_places_london diff --git a/test/fixtures/uk_ordnance_survey_places_london b/test/fixtures/uk_ordnance_survey_places_london new file mode 100644 index 000000000..68574aebc --- /dev/null +++ b/test/fixtures/uk_ordnance_survey_places_london @@ -0,0 +1,55 @@ +{ + "header": { + "uri": "https://api.os.uk/search/places/v1/find?query=London&dataset=DPA%2CLPI&output_srs=EPSG%3A4326&maxresults=1", + "query": "query=London", + "offset": 0, + "totalresults": 5734503, + "format": "JSON", + "dataset": "DPA,LPI", + "lr": "EN,CY", + "maxresults": 1, + "matchprecision": 1, + "epoch": "115", + "lastupdate": "2025-01-27", + "output_srs": "EPSG:4326" + }, + "results": [ + { + "DPA": { + "UPRN": "10009169128", + "UDPRN": "21361575", + "ADDRESS": "LONDON HOUSE, PENCADER, SA39 9AA", + "BUILDING_NAME": "LONDON HOUSE", + "POST_TOWN": "PENCADER", + "POSTCODE": "SA39 9AA", + "RPC": "1", + "X_COORDINATE": 244622.42, + "Y_COORDINATE": 236290.46, + "LNG": -4.2649815, + "LAT": 52.003258, + "STATUS": "APPROVED", + "LOGICAL_STATUS_CODE": "1", + "CLASSIFICATION_CODE": "RD04", + "CLASSIFICATION_CODE_DESCRIPTION": "Terraced", + "LOCAL_CUSTODIAN_CODE": 6825, + "LOCAL_CUSTODIAN_CODE_DESCRIPTION": "CARMARTHENSHIRE", + "COUNTRY_CODE": "W", + "COUNTRY_CODE_DESCRIPTION": "This record is within Wales", + "POSTAL_ADDRESS_CODE": "D", + "POSTAL_ADDRESS_CODE_DESCRIPTION": "A record which is linked to PAF", + "BLPU_STATE_CODE": "2", + "BLPU_STATE_CODE_DESCRIPTION": "In use", + "TOPOGRAPHY_LAYER_TOID": "osgb1000020500141", + "WARD_CODE": "W05001207", + "PARISH_CODE": "W04000523", + "LAST_UPDATE_DATE": "25/03/2022", + "ENTRY_DATE": "24/06/2004", + "BLPU_STATE_DATE": "24/06/2004", + "LANGUAGE": "CY", + "MATCH": 0.3, + "MATCH_DESCRIPTION": "NO MATCH", + "DELIVERY_POINT_SUFFIX": "2A" + } + } + ] +} \ No newline at end of file From 541fe3605830df8a958dd0bac40d74b59cfa5c57 Mon Sep 17 00:00:00 2001 From: Michael Agra Date: Tue, 28 Jan 2025 15:56:08 +0000 Subject: [PATCH 3/4] Remove country code filtering --- lib/geocoder/lookups/uk_ordnance_survey_places.rb | 11 +---------- lib/geocoder/results/uk_ordnance_survey_places.rb | 1 + 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/lib/geocoder/lookups/uk_ordnance_survey_places.rb b/lib/geocoder/lookups/uk_ordnance_survey_places.rb index ceff57eca..9af46b381 100644 --- a/lib/geocoder/lookups/uk_ordnance_survey_places.rb +++ b/lib/geocoder/lookups/uk_ordnance_survey_places.rb @@ -37,17 +37,8 @@ def query_url_params(query) query: query.sanitized_text, key: configuration.api_key, dataset: 'DPA,LPI', - output_srs: 'EPSG:4326', - fq: filter + output_srs: 'EPSG:4326' }.merge(super) end - - def country_codes - %w[E W S N L M] - end - - def filter - country_codes.map { |t| "COUNTRY_CODE:#{t}" }.join(' ') - end end end diff --git a/lib/geocoder/results/uk_ordnance_survey_places.rb b/lib/geocoder/results/uk_ordnance_survey_places.rb index 6275b288a..39e78cd0a 100644 --- a/lib/geocoder/results/uk_ordnance_survey_places.rb +++ b/lib/geocoder/results/uk_ordnance_survey_places.rb @@ -33,6 +33,7 @@ def province 'N' => 'Northern Ireland', 'L' => 'Channel Islands', 'M' => 'Isle of Man', + 'J' => '' }[data['COUNTRY_CODE']] end From ed6a94fe81d3d6e3118117bebd1a39f1b632022f Mon Sep 17 00:00:00 2001 From: Michael Agra Date: Wed, 29 Jan 2025 09:17:02 +0000 Subject: [PATCH 4/4] Limit response to 10 results --- lib/geocoder/lookups/uk_ordnance_survey_places.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/geocoder/lookups/uk_ordnance_survey_places.rb b/lib/geocoder/lookups/uk_ordnance_survey_places.rb index 9af46b381..e08005859 100644 --- a/lib/geocoder/lookups/uk_ordnance_survey_places.rb +++ b/lib/geocoder/lookups/uk_ordnance_survey_places.rb @@ -37,7 +37,8 @@ def query_url_params(query) query: query.sanitized_text, key: configuration.api_key, dataset: 'DPA,LPI', - output_srs: 'EPSG:4326' + output_srs: 'EPSG:4326', + maxresults: 10 }.merge(super) end end