Coverage for gws-app/gws/plugin/nominatim/model.py: 0%
69 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-05 13:35 +0200
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-05 13:35 +0200
1"""Nominatim model."""
3from typing import Optional
5import gws
6import gws.base.model
7import gws.base.feature
8import gws.lib.shape
9import gws.lib.net
10import gws.lib.jsonx
11import gws.gis.source
12import gws.lib.crs
13import gws.lib.bounds
16@gws.ext.config.model('nominatim')
17class Config(gws.base.model.Config):
18 """Data model for places and addresses from Nominatim."""
20 country: Optional[str]
21 """Countries to limit the results to."""
22 language: Optional[str]
23 """Preferred language of the results."""
26@gws.ext.object.model('nominatim')
27class Object(gws.base.model.default_model.Object):
28 """Nominatim model object.
30 A read-only model whose features are results of the Nominatim search API.
31 """
33 country: str
34 """Countries to limit the results to."""
35 language: str
36 """Preferred language of the results."""
38 serviceUrl = 'https://nominatim.openstreetmap.org/search'
40 def configure(self):
41 self.country = self.cfg('country')
42 self.language = self.cfg('language')
43 self.uidName = 'uid'
44 self.geometryName = 'geometry'
45 self.loadingStrategy = gws.FeatureLoadingStrategy.all
47 def props(self, user):
48 return gws.u.merge(
49 super().props(user),
50 canCreate=False,
51 canDelete=False,
52 canWrite=False,
53 )
55 def find_features(self, search, user, **kwargs):
56 params = {
57 'q': search.keyword,
58 'addressdetails': 1,
59 'polygon_geojson': 1,
60 'format': 'json',
61 'bounded': 1,
62 'dedupe': 1,
63 'limit': search.limit,
64 }
66 if self.language:
67 params['accept-language'] = self.cfg('language')
69 if self.country:
70 params['countrycodes'] = self.cfg('country')
72 params['viewbox'] = gws.lib.bounds.transform(search.shape.bounds(), gws.lib.crs.WGS84).extent
74 features = []
76 for rec in self._query(params):
77 uid = rec.get('place_id') or rec.get('osm_id')
78 geom = rec.pop('geojson', {})
80 if not geom:
81 gws.log.debug(f'SKIP {uid}: no geometry')
82 continue
84 shape = gws.lib.shape.from_geojson(geom, gws.lib.crs.WGS84, always_xy=True).transformed_to(search.shape.crs)
85 if not shape.intersects(search.shape):
86 gws.log.debug(f'SKIP {uid}: no intersection')
87 continue
89 features.append(self.feature_from_record(
90 gws.FeatureRecord(uid=uid, shape=shape, attributes=self._normalize(rec)),
91 user))
93 return sorted(features, key=lambda f: (f.get('name'), f.get('osm_class'), f.get('osm_type')))
95 def _query(self, params) -> list[dict]:
96 """Send a search request to the service and return the results, or an empty list on error."""
97 try:
98 res = gws.lib.net.http_request(self.serviceUrl, params=params)
99 return gws.lib.jsonx.from_string(res.text)
100 except gws.lib.net.Error as e:
101 gws.log.error('nominatim request error', e)
102 return []
104 def _normalize(self, rec):
105 """Flatten the address, add name and OSM class/type attributes, and drop coordinate attributes."""
106 # merge the address subrec into the main
108 if 'address' in rec:
109 for k, v in rec.pop('address').items():
110 rec['address_' + k] = v
112 # ensure basic address fields
114 rec['address_road'] = rec.get('address_road') or ''
115 rec['address_building'] = rec.get('address_building') or rec.get('address_house_number') or ''
116 rec['address_city'] = rec.get('address_city') or rec.get('address_town') or rec.get('address_village') or ''
117 rec['address_country'] = rec.get('address_country') or ''
119 # find out the "name"
120 # the problem is there's no fixed key for the name (the key depends on the class)
121 # however, display_name seems to always start with the 'name'
123 dn = rec.get('display_name', '')
124 rec['name'] = dn.split(',')[0].strip()
126 # rename 'class' and 'type' for easier templating
128 rec['osm_class'] = rec.pop('class', '')
129 rec['osm_type'] = rec.pop('type', '')
131 # remove geographic attributes
133 for k in 'boundingbox', 'lat', 'lon':
134 rec.pop(k, None)
136 return rec