1 -- SPDX-License-Identifier: GPL-2.0-only
3 -- This file is part of Nominatim. (https://nominatim.org)
5 -- Copyright (C) 2026 by the Nominatim developer community.
6 -- For a full list of authors see the git log.
8 -- Functions for address interpolation objects in location_property_osmline.
10 CREATE OR REPLACE FUNCTION place_interpolation_insert()
15 existingplacex BIGINT[];
18 IF NOT (NEW.type in ('odd', 'even', 'all') OR NEW.type similar to '[1-9]') THEN
19 -- the new interpolation is illegal, simply remove existing entries
20 DELETE FROM location_property_osmline o WHERE o.osm_id = NEW.osm_id;
24 -- Remove the place from the list of places to be deleted
25 DELETE FROM place_interpolation_to_be_deleted pdel WHERE pdel.osm_id = NEW.osm_id;
27 SELECT * INTO existing FROM place_interpolation p WHERE p.osm_id = NEW.osm_id;
29 -- Get the existing entry from the interpolation table.
30 SELECT array_agg(place_id) INTO existingplacex
31 FROM location_property_osmline o WHERE o.osm_id = NEW.osm_id;
33 IF array_length(existingplacex, 1) is NULL THEN
34 INSERT INTO location_property_osmline (osm_id, type, address, linegeo)
35 VALUES (NEW.osm_id, NEW.type, NEW.address, NEW.geometry);
37 -- Update the interpolation table:
38 -- The first entry gets the original data, all other entries
39 -- are removed and will be recreated on indexing.
40 -- (An interpolation can be split up, if it has more than 2 address nodes)
41 -- Update unconditionally here as the changes might be coming from the
42 -- nodes on the interpolation.
43 UPDATE location_property_osmline
45 address = NEW.address,
46 linegeo = NEW.geometry,
49 WHERE place_id = existingplacex[1];
50 IF array_length(existingplacex, 1) > 1 THEN
51 DELETE FROM location_property_osmline WHERE place_id = any(existingplacex[2:]);
55 -- need to invalidate nodes because they might copy address info
56 IF NEW.address is not NULL
57 AND (existing.osm_id is NULL
58 OR coalesce(existing.address, ''::hstore) != NEW.address)
60 UPDATE placex SET indexed_status = 2
61 WHERE osm_type = 'N' AND osm_id = ANY(NEW.nodes) AND indexed_status = 0;
64 -- finally update/insert place_interpolation itself
66 IF existing.osm_id is not NULL THEN
67 -- Always updates as the nodes with the housenumber might be the reason
69 UPDATE place_interpolation p
71 address = NEW.address,
73 geometry = NEW.geometry
74 WHERE p.osm_id = NEW.osm_id;
84 CREATE OR REPLACE FUNCTION place_interpolation_delete()
90 {% if debug %}RAISE WARNING 'Delete for interpolation %', OLD.osm_id;{% endif %}
92 INSERT INTO place_interpolation_to_be_deleted (osm_id) VALUES(OLD.osm_id);
99 CREATE OR REPLACE FUNCTION get_interpolation_address(in_address HSTORE, wayid BIGINT)
106 IF in_address ? 'street' or in_address ? 'place' THEN
110 SELECT nodes INTO waynodes FROM place_interpolation WHERE osm_id = wayid;
112 IF array_upper(waynodes, 1) IS NOT NULL THEN
114 SELECT placex.address, placex.osm_id FROM placex
115 WHERE osm_type = 'N' and osm_id = ANY(waynodes)
116 and placex.address is not null
117 and (placex.address ? 'street' or placex.address ? 'place')
118 and indexed_status < 100
120 -- mark it as a derived address
121 RETURN location.address || coalesce(in_address, ''::hstore) || hstore('_inherited', '');
128 LANGUAGE plpgsql STABLE PARALLEL SAFE;
132 -- find the parent road of the cut road parts
133 CREATE OR REPLACE FUNCTION get_interpolation_parent(token_info JSONB,
135 centroid GEOMETRY, geom GEOMETRY)
139 parent_place_id BIGINT;
142 parent_place_id := find_parent_for_address(token_info, partition, centroid);
144 IF parent_place_id is null THEN
145 FOR location IN SELECT place_id FROM placex
146 WHERE ST_DWithin(geom, placex.geometry, 0.001)
147 and placex.rank_search = 26
148 and placex.osm_type = 'W' -- needed for index selection
149 ORDER BY CASE WHEN ST_GeometryType(geom) = 'ST_Line' THEN
150 (ST_distance(placex.geometry, ST_LineInterpolatePoint(geom,0))+
151 ST_distance(placex.geometry, ST_LineInterpolatePoint(geom,0.5))+
152 ST_distance(placex.geometry, ST_LineInterpolatePoint(geom,1)))
153 ELSE ST_distance(placex.geometry, geom) END
157 parent_place_id := location.place_id;
161 RETURN parent_place_id;
164 LANGUAGE plpgsql STABLE PARALLEL SAFE;
167 CREATE OR REPLACE FUNCTION osmline_insert()
173 NEW.place_id := nextval('seq_place');
174 NEW.indexed_date := now();
176 IF NEW.indexed_status IS NULL THEN
177 IF NOT(NEW.type in ('odd', 'even', 'all') OR NEW.type similar to '[1-9]') THEN
178 -- alphabetic interpolation is not supported
182 centroid := get_center_point(NEW.linegeo);
183 NEW.indexed_status := 1; --STATUS_NEW
184 NEW.country_code := lower(get_country_code(centroid));
186 NEW.partition := get_partition(NEW.country_code);
187 NEW.geometry_sector := geometry_sector(NEW.partition, centroid);
196 CREATE OR REPLACE FUNCTION osmline_update()
215 IF OLD.indexed_status = 100 THEN
216 delete from location_property_osmline where place_id = OLD.place_id;
220 IF NEW.indexed_status != 0 OR OLD.indexed_status = 0 THEN
224 NEW.parent_place_id := get_interpolation_parent(NEW.token_info, NEW.partition,
225 get_center_point(NEW.linegeo),
228 NEW.token_info := token_strip_info(NEW.token_info);
229 IF NEW.address ? '_inherited' THEN
233 -- If the line was newly inserted, split the line as necessary.
234 IF NEW.parent_place_id is not NULL AND NEW.startnumber is NULL THEN
235 IF NEW.type in ('odd', 'even') THEN
237 stepmod := CASE WHEN NEW.type = 'odd' THEN 1 ELSE 0 END;
239 NEW.step := CASE WHEN NEW.type = 'all' THEN 1 ELSE (NEW.type)::SMALLINT END;
243 SELECT nodes INTO waynodes FROM place_interpolation WHERE osm_id = NEW.osm_id;
245 IF array_upper(waynodes, 1) IS NULL THEN
250 SELECT null::integer as hnr INTO prevnode;
252 -- Go through all nodes on the interpolation line that have a housenumber.
254 SELECT DISTINCT ON (nodeidpos)
255 osm_id, address, geometry,
256 -- Take the postcode from the node only if it has a housenumber itself.
257 -- Note that there is a corner-case where the node has a wrongly
258 -- formatted postcode and therefore 'postcode' contains a derived
260 CASE WHEN address ? 'postcode' THEN placex.postcode ELSE NULL::text END as postcode,
261 (address->'housenumber')::integer as hnr
262 FROM placex, generate_series(1, array_upper(waynodes, 1)) nodeidpos
263 WHERE osm_type = 'N' and osm_id = waynodes[nodeidpos]::BIGINT
264 and address is not NULL and address ? 'housenumber'
265 and address->'housenumber' ~ '^[0-9]{1,6}$'
266 and ST_Distance(NEW.linegeo, geometry) < 0.0005
269 {% if debug %}RAISE WARNING 'processing point % (%)', nextnode.hnr, ST_AsText(nextnode.geometry);{% endif %}
270 IF linegeo is null THEN
271 linegeo := NEW.linegeo;
273 splitpoint := ST_LineLocatePoint(linegeo, nextnode.geometry);
274 IF splitpoint = 0 THEN
275 -- Corner case where the splitpoint falls on the first point
276 -- and thus would not return a geometry. Skip that section.
278 ELSEIF splitpoint = 1 THEN
279 -- Point is at the end of the line.
280 sectiongeo := linegeo;
284 sectiongeo := ST_LineSubstring(linegeo, 0, splitpoint);
285 linegeo := ST_LineSubstring(linegeo, splitpoint, 1);
289 IF prevnode.hnr is not null
290 -- Check if there are housenumbers to interpolate between the
291 -- regularly mapped housenumbers.
292 -- (Conveniently also fails if one of the house numbers is not a number.)
293 and abs(prevnode.hnr - nextnode.hnr) > NEW.step
294 -- If the interpolation geometry is broken or two nodes are at the
295 -- same place, then splitting might produce a point. Ignore that.
296 and ST_GeometryType(sectiongeo) = 'ST_LineString'
298 IF prevnode.hnr < nextnode.hnr THEN
299 startnumber := prevnode.hnr;
300 endnumber := nextnode.hnr;
302 startnumber := nextnode.hnr;
303 endnumber := prevnode.hnr;
304 sectiongeo := ST_Reverse(sectiongeo);
307 -- Adjust the interpolation, so that only inner housenumbers
308 -- are taken into account.
309 IF stepmod is null THEN
310 newstart := startnumber + NEW.step;
312 newstart := startnumber + 1;
313 moddiff := newstart % NEW.step - stepmod;
315 newstart := newstart + (NEW.step + moddiff);
317 newstart := newstart + moddiff;
320 newend := newstart + ((endnumber - 1 - newstart) / NEW.step) * NEW.step;
322 -- If newstart and newend are the same, then this returns a point.
323 sectiongeo := ST_LineSubstring(sectiongeo,
324 (newstart - startnumber)::float / (endnumber - startnumber)::float,
325 (newend - startnumber)::float / (endnumber - startnumber)::float);
326 startnumber := newstart;
329 -- determine postcode
330 postcode := coalesce(prevnode.postcode, nextnode.postcode, postcode);
331 IF postcode is NULL and NEW.parent_place_id > 0 THEN
332 SELECT placex.postcode FROM placex
333 WHERE place_id = NEW.parent_place_id INTO postcode;
335 IF postcode is NULL THEN
336 postcode := get_nearest_postcode(NEW.country_code, nextnode.geometry);
339 -- Add the interpolation. If this is the first segment, just modify
340 -- the interpolation to be inserted, otherwise add an additional one
341 -- (marking it indexed already).
342 IF NEW.startnumber IS NULL THEN
343 NEW.startnumber := startnumber;
344 NEW.endnumber := endnumber;
345 NEW.linegeo := ST_ReducePrecision(sectiongeo, 0.0000001);
346 NEW.postcode := postcode;
348 INSERT INTO location_property_osmline
349 (linegeo, partition, osm_id, parent_place_id,
350 startnumber, endnumber, step, type,
351 address, postcode, country_code,
352 geometry_sector, indexed_status)
353 VALUES (ST_ReducePrecision(sectiongeo, 0.0000001),
354 NEW.partition, NEW.osm_id, NEW.parent_place_id,
355 startnumber, endnumber, NEW.step, NEW.type,
356 NEW.address, postcode,
357 NEW.country_code, NEW.geometry_sector, 0);
361 -- early break if we are out of line string,
362 -- might happen when a line string loops back on itself
363 IF linegeo is null or ST_GeometryType(linegeo) != 'ST_LineString' THEN
367 prevnode := nextnode;