1 # SPDX-License-Identifier: GPL-2.0-only
3 # This file is part of Nominatim. (https://nominatim.org)
5 # Copyright (C) 2023 by the Nominatim developer community.
6 # For a full list of authors see the git log.
8 Output formatters for API version v1.
10 from typing import List, Dict, Mapping, Any
13 import nominatim.api as napi
14 from nominatim.api.result_formatting import FormatDispatcher
15 from nominatim.api.v1.classtypes import ICONS
16 from nominatim.api.v1 import format_json, format_xml
17 from nominatim.utils.json_writer import JsonWriter
19 class RawDataList(List[Dict[str, Any]]):
20 """ Data type for formatting raw data lists 'as is' in json.
23 dispatch = FormatDispatcher()
25 @dispatch.format_func(napi.StatusResult, 'text')
26 def _format_status_text(result: napi.StatusResult, _: Mapping[str, Any]) -> str:
28 return f"ERROR: {result.message}"
33 @dispatch.format_func(napi.StatusResult, 'json')
34 def _format_status_json(result: napi.StatusResult, _: Mapping[str, Any]) -> str:
38 .keyval('status', result.status)\
39 .keyval('message', result.message)\
40 .keyval_not_none('data_updated', result.data_updated,
41 lambda v: v.isoformat())\
42 .keyval('software_version', str(result.software_version))\
43 .keyval_not_none('database_version', result.database_version, str)\
49 def _add_address_row(writer: JsonWriter, row: napi.AddressLine,
50 locales: napi.Locales) -> None:
51 writer.start_object()\
52 .keyval('localname', locales.display_name(row.names))\
53 .keyval_not_none('place_id', row.place_id)
55 if row.osm_object is not None:
56 writer.keyval('osm_id', row.osm_object[1])\
57 .keyval('osm_type', row.osm_object[0])
60 writer.keyval_not_none('place_type', row.extratags.get('place_type'))
62 writer.keyval('class', row.category[0])\
63 .keyval('type', row.category[1])\
64 .keyval_not_none('admin_level', row.admin_level)\
65 .keyval('rank_address', row.rank_address)\
66 .keyval('distance', row.distance)\
67 .keyval('isaddress', row.isaddress)\
71 def _add_address_rows(writer: JsonWriter, section: str, rows: napi.AddressLines,
72 locales: napi.Locales) -> None:
73 writer.key(section).start_array()
75 _add_address_row(writer, row, locales)
77 writer.end_array().next()
80 def _add_parent_rows_grouped(writer: JsonWriter, rows: napi.AddressLines,
81 locales: napi.Locales) -> None:
82 # group by category type
83 data = collections.defaultdict(list)
86 _add_address_row(sub, row, locales)
87 data[row.category[1]].append(sub())
89 writer.key('hierarchy').start_object()
90 for group, grouped in data.items():
91 writer.key(group).start_array()
92 grouped.sort() # sorts alphabetically by local name
94 writer.raw(line).next()
95 writer.end_array().next()
97 writer.end_object().next()
100 @dispatch.format_func(napi.DetailedResult, 'json')
101 def _format_details_json(result: napi.DetailedResult, options: Mapping[str, Any]) -> str:
102 locales = options.get('locales', napi.Locales())
103 geom = result.geometry.get('geojson')
104 centroid = result.centroid.to_geojson()
108 .keyval_not_none('place_id', result.place_id)\
109 .keyval_not_none('parent_place_id', result.parent_place_id)
111 if result.osm_object is not None:
112 out.keyval('osm_type', result.osm_object[0])\
113 .keyval('osm_id', result.osm_object[1])
115 out.keyval('category', result.category[0])\
116 .keyval('type', result.category[1])\
117 .keyval('admin_level', result.admin_level)\
118 .keyval('localname', result.locale_name or '')\
119 .keyval('names', result.names or {})\
120 .keyval('addresstags', result.address or {})\
121 .keyval_not_none('housenumber', result.housenumber)\
122 .keyval_not_none('calculated_postcode', result.postcode)\
123 .keyval_not_none('country_code', result.country_code)\
124 .keyval_not_none('indexed_date', result.indexed_date, lambda v: v.isoformat())\
125 .keyval_not_none('importance', result.importance)\
126 .keyval('calculated_importance', result.calculated_importance())\
127 .keyval('extratags', result.extratags or {})\
128 .keyval_not_none('calculated_wikipedia', result.wikipedia)\
129 .keyval('rank_address', result.rank_address)\
130 .keyval('rank_search', result.rank_search)\
131 .keyval('isarea', 'Polygon' in (geom or result.geometry.get('type') or ''))\
132 .key('centroid').raw(centroid).next()\
133 .key('geometry').raw(geom or centroid).next()
135 if options.get('icon_base_url', None):
136 icon = ICONS.get(result.category)
138 out.keyval('icon', f"{options['icon_base_url']}/{icon}.p.20.png")
140 if result.address_rows is not None:
141 _add_address_rows(out, 'address', result.address_rows, locales)
143 if result.linked_rows is not None:
144 _add_address_rows(out, 'linked_places', result.linked_rows, locales)
146 if result.name_keywords is not None or result.address_keywords is not None:
147 out.key('keywords').start_object()
149 for sec, klist in (('name', result.name_keywords), ('address', result.address_keywords)):
150 out.key(sec).start_array()
151 for word in (klist or []):
153 .keyval('id', word.word_id)\
154 .keyval('token', word.word_token)\
156 out.end_array().next()
158 out.end_object().next()
160 if result.parented_rows is not None:
161 if options.get('group_hierarchy', False):
162 _add_parent_rows_grouped(out, result.parented_rows, locales)
164 _add_address_rows(out, 'hierarchy', result.parented_rows, locales)
171 @dispatch.format_func(napi.ReverseResults, 'xml')
172 def _format_reverse_xml(results: napi.ReverseResults, options: Mapping[str, Any]) -> str:
173 return format_xml.format_base_xml(results,
174 options, True, 'reversegeocode',
175 {'querystring': options.get('query', '')})
178 @dispatch.format_func(napi.ReverseResults, 'geojson')
179 def _format_reverse_geojson(results: napi.ReverseResults,
180 options: Mapping[str, Any]) -> str:
181 return format_json.format_base_geojson(results, options, True)
184 @dispatch.format_func(napi.ReverseResults, 'geocodejson')
185 def _format_reverse_geocodejson(results: napi.ReverseResults,
186 options: Mapping[str, Any]) -> str:
187 return format_json.format_base_geocodejson(results, options, True)
190 @dispatch.format_func(napi.ReverseResults, 'json')
191 def _format_reverse_json(results: napi.ReverseResults,
192 options: Mapping[str, Any]) -> str:
193 return format_json.format_base_json(results, options, True,
197 @dispatch.format_func(napi.ReverseResults, 'jsonv2')
198 def _format_reverse_jsonv2(results: napi.ReverseResults,
199 options: Mapping[str, Any]) -> str:
200 return format_json.format_base_json(results, options, True,
201 class_label='category')
204 @dispatch.format_func(napi.SearchResults, 'xml')
205 def _format_search_xml(results: napi.SearchResults, options: Mapping[str, Any]) -> str:
206 extra = {'querystring': options.get('query', '')}
207 for attr in ('more_url', 'exclude_place_ids', 'viewbox'):
208 if options.get(attr):
209 extra[attr] = options[attr]
210 return format_xml.format_base_xml(results, options, False, 'searchresults',
215 @dispatch.format_func(napi.SearchResults, 'geojson')
216 def _format_search_geojson(results: napi.SearchResults,
217 options: Mapping[str, Any]) -> str:
218 return format_json.format_base_geojson(results, options, False)
221 @dispatch.format_func(napi.SearchResults, 'geocodejson')
222 def _format_search_geocodejson(results: napi.SearchResults,
223 options: Mapping[str, Any]) -> str:
224 return format_json.format_base_geocodejson(results, options, False)
227 @dispatch.format_func(napi.SearchResults, 'json')
228 def _format_search_json(results: napi.SearchResults,
229 options: Mapping[str, Any]) -> str:
230 return format_json.format_base_json(results, options, False,
234 @dispatch.format_func(napi.SearchResults, 'jsonv2')
235 def _format_search_jsonv2(results: napi.SearchResults,
236 options: Mapping[str, Any]) -> str:
237 return format_json.format_base_json(results, options, False,
238 class_label='category')
240 @dispatch.format_func(RawDataList, 'json')
241 def _format_raw_data_json(results: RawDataList, _: Mapping[str, Any]) -> str:
246 for k, v in res.items():
248 out.end_object().next()