From 5fe3ff898ad90defc63db279af1a12d3a3ca5325 Mon Sep 17 00:00:00 2001 From: Paige Williams Date: Mon, 31 Aug 2026 16:22:08 -0700 Subject: [PATCH 1/3] use osm geocoder instead of arcgis --- landmapper/app/views.py | 146 +++++++++++++++++------------- landmapper/landmapper/settings.py | 21 +++-- 2 files changed, 96 insertions(+), 71 deletions(-) diff --git a/landmapper/app/views.py b/landmapper/app/views.py index 1f3a35f..94f623e 100644 --- a/landmapper/app/views.py +++ b/landmapper/app/views.py @@ -77,7 +77,7 @@ def unstable_request_wrapper(url, params=False, retries=0): contents = False return contents -def geocode(search_string, srs=4326, service='arcgis', with_context=False): +def geocode(search_string, srs=4326, service='osm', with_context=False): # """ # geocode # PURPOSE: Convert a provided place name into geographic coordinates @@ -86,10 +86,11 @@ def geocode(search_string, srs=4326, service='arcgis', with_context=False): # - srs: (int) The EPSG ID for the spatial reference system in which to output coordinates # - defaut: 4326 # - service: (string) The geocoding service to query for a result - # - default = 'arcgis' - # - other supported options: 'google' + # - default = 'osm' (Open Street Map Nominatim) + # - 'arcgis' (ArcGIS World Geocoding Service) + # - with_context: (bool) Whether to include contextual hits if no direct hits are found # OUT: - # - coords: a list of two coordinate elements -- [lat(y), long(x)] + # - coords: a list of two coordinate elements -- [lat(y), lon(x)] # - projected in the requested coordinate system # CALLED BY: # - identify @@ -99,64 +100,87 @@ def geocode(search_string, srs=4326, service='arcgis', with_context=False): g_hits = [] hit_names = [] - # Query desired service - # TODO: support more than just ArcGIS supplied geocodes - # Use other services if no matches are found. - # if service.lower() == 'arcgis': - g_matches = geocoder.arcgis(search_string, maxRows=100) - for match in g_matches: - if (match.latlng[0] <= settings.STUDY_REGION['north'] and - match.latlng[0] >= settings.STUDY_REGION['south'] and - match.latlng[1] <= settings.STUDY_REGION['east'] and - match.latlng[1] >= settings.STUDY_REGION['west']): - if not match.raw['name'] in hit_names: - g_hits.append(match) - hit_names.append(match.raw['name']) - for hit in g_hits: - hits.append({ - 'name': hit.raw['name'], - 'coords': hit.latlng, - 'confidence': hit.confidence - }) - - if not with_context: - - if len(hits) == 0: - for context in settings.STUDY_REGION['context']: - new_hits = geocode("%s%s" % (search_string, context), srs=srs, service=service, with_context=True) - for hit in new_hits: - if not hit['name'] in hit_names: - hits.append(hit) - hit_names.append(hit['name']) - # TODO: If new hits match but have better confidence, replace - - # Transform coordinates if necessary - if not srs == 4326: - - if ':' in srs: + if service.lower() == 'arcgis': + g_matches = geocoder.arcgis(search_string, maxRows=100) + for match in g_matches: + if (match.latlng[0] <= settings.STUDY_REGION['north'] and + match.latlng[0] >= settings.STUDY_REGION['south'] and + match.latlng[1] <= settings.STUDY_REGION['east'] and + match.latlng[1] >= settings.STUDY_REGION['west']): + if not match.raw['name'] in hit_names: + g_hits.append(match) + hit_names.append(match.raw['name']) + for hit in g_hits: + hits.append({ + 'name': hit.raw['name'], + 'coords': hit.latlng, + 'confidence': hit.confidence + }) + + if not with_context: + + if len(hits) == 0: + for context in settings.STUDY_REGION['context']: + new_hits = geocode("%s%s" % (search_string, context), srs=srs, service=service, with_context=True) + for hit in new_hits: + if not hit['name'] in hit_names: + hits.append(hit) + hit_names.append(hit['name']) + # TODO: If new hits match but have better confidence, replace + + # Transform coordinates if necessary + if not srs == 4326: + + if ':' in srs: + try: + srs = srs.split(':')[1] + except Exception as e: + pass try: - srs = srs.split(':')[1] - except Exception as e: - pass - try: - int(srs) - except ValueError as e: - print( - 'ERROR: Unable to interpret provided srs. Please provide a valid EPSG integer ID. Providing coords in EPSG:4326' - ) - return coords - - hits_transform = [] - for hit in hits: - coords = hit.latlng - point = GEOSGeometry('SRID=4326;POINT (%s %s)' % - (coords[1], coords[0]), - srid=4326) - point.transform(srs) - coords = [point.coords[1], point.coords[0]] - hits_transform.append(coords) - hits = hits_transfrom - + int(srs) + except ValueError as e: + print( + 'ERROR: Unable to interpret provided srs. Please provide a valid EPSG integer ID. Providing coords in EPSG:4326' + ) + return coords + + hits_transform = [] + for hit in hits: + coords = hit.latlng + point = GEOSGeometry('SRID=4326;POINT (%s %s)' % + (coords[1], coords[0]), + srid=4326) + point.transform(srs) + coords = [point.coords[1], point.coords[0]] + hits_transform.append(coords) + hits = hits_transform + + elif service.lower() == 'osm': + headers = { + "User-Agent": "Landmapper (Contact: ksdev@ecotrust.org)", + "Referer": "https://landmapper.ecotrust.org/" + } + g_matches = requests.get('https://nominatim.openstreetmap.org/search', params={'q': search_string, 'format': 'json', 'limit': 100}, headers=headers).json() + for match in g_matches: + if (float(match['lat']) < settings.STUDY_REGION['north'] and + float(match['lat']) > settings.STUDY_REGION['south'] and + float(match['lon']) < settings.STUDY_REGION['east'] and + float(match['lon']) > settings.STUDY_REGION['west']): + if not match['display_name'] in hit_names: + g_hits.append(match) + hit_names.append(match['display_name']) + for hit in g_hits: + hits.append({ + 'name': hit['display_name'], + 'coords': [float(hit['lat']), float(hit['lon'])], + 'confidence': hit.get('importance', 0) + }) + + if not with_context: + if len(hits) == 0: + # default to the center of the state if no hits are found and with_context is False + hits.append(settings.STUDY_REGION['context']) + hits = sorted(hits, key = lambda i: i['confidence'], reverse=True) if len(hits) > 5: hits = hits[:5] diff --git a/landmapper/landmapper/settings.py b/landmapper/landmapper/settings.py index 24ef8e3..e5b9d6f 100644 --- a/landmapper/landmapper/settings.py +++ b/landmapper/landmapper/settings.py @@ -257,11 +257,12 @@ 'forest_map_legend_url': '/static/landmapper/img/legend_images/OR_forest_type.png', 'forest_canopy_legend_url': '/static/landmapper/img/legend_images/OR_forest_canopy.png', 'forest_size_legend_url': '/static/landmapper/img/legend_images/OR_forest_size.png', - 'forest_density_legend_url': '/static/landmapper/img/legend_images/OR_forest_density.png', - 'context': [ - ', OR', - ', Oregon USA', - ] + 'forest_density_legend_url': '/static/landmapper/img/legend_images/OR_forest_density.png', + 'context': { + 'name': 'Oregon', + 'coords': [43.9909179,-120.6359984], + 'confidence': 1 + } }, 'WA': { 'north': 49.002405, @@ -286,11 +287,11 @@ 'forest_canopy_legend_url': '/static/landmapper/img/legend_images/WA_forest_canopy.png', 'forest_size_legend_url': '/static/landmapper/img/legend_images/WA_forest_size.png', 'forest_density_legend_url': '/static/landmapper/img/legend_images/WA_forest_density.png', - 'context': [ - ', WA', - ', Washington USA', - # ', WA', - ] + 'context': { + 'name': 'Washington', + 'coords': [47.273118,-120.824902], + 'confidence': 1 + } } } From 47c697c9f2d30a0dbd43574d67f26af6f5c640f3 Mon Sep 17 00:00:00 2001 From: Paige Williams Date: Tue, 1 Sep 2026 10:29:45 -0700 Subject: [PATCH 2/3] support multiple locations in the STUDY_REGION['context'] --- landmapper/app/views.py | 2 +- landmapper/landmapper/settings.py | 34 ++++++++++++++++++++++--------- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/landmapper/app/views.py b/landmapper/app/views.py index 94f623e..e1fd603 100644 --- a/landmapper/app/views.py +++ b/landmapper/app/views.py @@ -179,7 +179,7 @@ def geocode(search_string, srs=4326, service='osm', with_context=False): if not with_context: if len(hits) == 0: # default to the center of the state if no hits are found and with_context is False - hits.append(settings.STUDY_REGION['context']) + hits.extend(settings.STUDY_REGION['context']) hits = sorted(hits, key = lambda i: i['confidence'], reverse=True) if len(hits) > 5: diff --git a/landmapper/landmapper/settings.py b/landmapper/landmapper/settings.py index e5b9d6f..eb52c13 100644 --- a/landmapper/landmapper/settings.py +++ b/landmapper/landmapper/settings.py @@ -258,11 +258,18 @@ 'forest_canopy_legend_url': '/static/landmapper/img/legend_images/OR_forest_canopy.png', 'forest_size_legend_url': '/static/landmapper/img/legend_images/OR_forest_size.png', 'forest_density_legend_url': '/static/landmapper/img/legend_images/OR_forest_density.png', - 'context': { - 'name': 'Oregon', - 'coords': [43.9909179,-120.6359984], - 'confidence': 1 - } + 'context': [ + { + 'name': 'OR', + 'coords': [43.9909179,-120.6359984], + 'confidence': 1 + }, + { + 'name': 'Oregon', + 'coords': [43.9909179,-120.6359984], + 'confidence': 1 + } + ] }, 'WA': { 'north': 49.002405, @@ -287,11 +294,18 @@ 'forest_canopy_legend_url': '/static/landmapper/img/legend_images/WA_forest_canopy.png', 'forest_size_legend_url': '/static/landmapper/img/legend_images/WA_forest_size.png', 'forest_density_legend_url': '/static/landmapper/img/legend_images/WA_forest_density.png', - 'context': { - 'name': 'Washington', - 'coords': [47.273118,-120.824902], - 'confidence': 1 - } + 'context': [ + { + 'name': 'WA', + 'coords': [47.273118,-120.824902], + 'confidence': 1 + }, + { + 'name': 'Washington', + 'coords': [47.273118,-120.824902], + 'confidence': 1 + } + ] } } From aa48af8f75d1c20d35172a36a4131fcdd05251ca Mon Sep 17 00:00:00 2001 From: Paige Williams Date: Tue, 1 Sep 2026 10:39:32 -0700 Subject: [PATCH 3/3] support STUDY_REGION.contextual_hits and STUDY_REGION.context --- landmapper/app/views.py | 2 +- landmapper/landmapper/settings.py | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/landmapper/app/views.py b/landmapper/app/views.py index e1fd603..6f568c3 100644 --- a/landmapper/app/views.py +++ b/landmapper/app/views.py @@ -179,7 +179,7 @@ def geocode(search_string, srs=4326, service='osm', with_context=False): if not with_context: if len(hits) == 0: # default to the center of the state if no hits are found and with_context is False - hits.extend(settings.STUDY_REGION['context']) + hits.extend(settings.STUDY_REGION['contextual_hits']) hits = sorted(hits, key = lambda i: i['confidence'], reverse=True) if len(hits) > 5: diff --git a/landmapper/landmapper/settings.py b/landmapper/landmapper/settings.py index eb52c13..48c8909 100644 --- a/landmapper/landmapper/settings.py +++ b/landmapper/landmapper/settings.py @@ -257,8 +257,12 @@ 'forest_map_legend_url': '/static/landmapper/img/legend_images/OR_forest_type.png', 'forest_canopy_legend_url': '/static/landmapper/img/legend_images/OR_forest_canopy.png', 'forest_size_legend_url': '/static/landmapper/img/legend_images/OR_forest_size.png', - 'forest_density_legend_url': '/static/landmapper/img/legend_images/OR_forest_density.png', + 'forest_density_legend_url': '/static/landmapper/img/legend_images/OR_forest_density.png', 'context': [ + ', OR', + ', Oregon USA', + ], + 'contextual_hits': [ { 'name': 'OR', 'coords': [43.9909179,-120.6359984], @@ -295,6 +299,10 @@ 'forest_size_legend_url': '/static/landmapper/img/legend_images/WA_forest_size.png', 'forest_density_legend_url': '/static/landmapper/img/legend_images/WA_forest_density.png', 'context': [ + ', WA', + ', Washington USA', + ], + 'contextual_hits': [ { 'name': 'WA', 'coords': [47.273118,-120.824902],