]> git.openstreetmap.org Git - nominatim.git/blob - lib-sql/functions/interpolation.sql
Switches associatedStreet handling to dedicated table
[nominatim.git] / lib-sql / functions / interpolation.sql
1 -- SPDX-License-Identifier: GPL-2.0-only
2 --
3 -- This file is part of Nominatim. (https://nominatim.org)
4 --
5 -- Copyright (C) 2026 by the Nominatim developer community.
6 -- For a full list of authors see the git log.
7
8 -- Functions for address interpolation objects in location_property_osmline.
9
10 CREATE OR REPLACE FUNCTION place_interpolation_insert()
11   RETURNS TRIGGER
12   AS $$
13 DECLARE
14   existing RECORD;
15   existingplacex BIGINT[];
16
17 BEGIN
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;
21     RETURN NULL;
22   END IF;
23
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;
26
27   SELECT * INTO existing FROM place_interpolation p WHERE p.osm_id = NEW.osm_id;
28
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;
32
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);
36   ELSE
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
44       SET type = NEW.type,
45           address = NEW.address,
46           linegeo = NEW.geometry,
47           startnumber = null,
48           indexed_status = 1
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:]);
52     END IF;
53   END IF;
54
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)
59   THEN
60     UPDATE placex SET indexed_status = 2
61       WHERE osm_type = 'N' AND osm_id = ANY(NEW.nodes) AND indexed_status = 0;
62   END IF;
63
64   -- finally update/insert place_interpolation itself
65
66   IF existing.osm_id is not NULL THEN
67     -- Always updates as the nodes with the housenumber might be the reason
68     -- for the change.
69     UPDATE place_interpolation p
70       SET type = NEW.type,
71           address = NEW.address,
72           nodes = NEW.nodes,
73           geometry = NEW.geometry
74       WHERE p.osm_id = NEW.osm_id;
75
76     RETURN NULL;
77   END IF;
78
79   RETURN NEW;
80 END;
81 $$ LANGUAGE plpgsql;
82
83
84 CREATE OR REPLACE FUNCTION place_interpolation_delete()
85   RETURNS TRIGGER
86   AS $$
87 DECLARE
88   deferred BOOLEAN;
89 BEGIN
90   {% if debug %}RAISE WARNING 'Delete for interpolation %', OLD.osm_id;{% endif %}
91
92   INSERT INTO place_interpolation_to_be_deleted (osm_id) VALUES(OLD.osm_id);
93
94   RETURN NULL;
95 END;
96 $$ LANGUAGE plpgsql;
97
98
99 CREATE OR REPLACE FUNCTION get_interpolation_address(in_address HSTORE, wayid BIGINT)
100 RETURNS HSTORE
101   AS $$
102 DECLARE
103   location RECORD;
104   waynodes BIGINT[];
105 BEGIN
106   IF in_address ? 'street' or in_address ? 'place' THEN
107     RETURN in_address;
108   END IF;
109
110   SELECT nodes INTO waynodes FROM place_interpolation WHERE osm_id = wayid;
111
112   IF array_upper(waynodes, 1) IS NOT NULL THEN
113     FOR location IN
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
119     LOOP
120       -- mark it as a derived address
121       RETURN location.address || coalesce(in_address, ''::hstore) || hstore('_inherited', '');
122     END LOOP;
123   END IF;
124
125   RETURN in_address;
126 END;
127 $$
128 LANGUAGE plpgsql STABLE PARALLEL SAFE;
129
130
131
132 -- find the parent road of the cut road parts
133 CREATE OR REPLACE FUNCTION get_interpolation_parent(token_info JSONB,
134                                                     partition SMALLINT,
135                                                     centroid GEOMETRY, geom GEOMETRY)
136   RETURNS BIGINT
137   AS $$
138 DECLARE
139   parent_place_id BIGINT;
140   location RECORD;
141 BEGIN
142   parent_place_id := find_parent_for_address(token_info, partition, centroid);
143
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
154               ASC
155         LIMIT 1
156     LOOP
157       parent_place_id := location.place_id;
158     END LOOP;
159   END IF;
160
161   RETURN parent_place_id;
162 END;
163 $$
164 LANGUAGE plpgsql STABLE PARALLEL SAFE;
165
166
167 CREATE OR REPLACE FUNCTION osmline_insert()
168   RETURNS TRIGGER
169   AS $$
170 DECLARE
171   centroid GEOMETRY;
172 BEGIN
173   NEW.place_id := nextval('seq_place');
174   NEW.indexed_date := now();
175
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
179         RETURN NULL;
180     END IF;
181
182     centroid := get_center_point(NEW.linegeo);
183     NEW.indexed_status := 1; --STATUS_NEW
184     NEW.country_code := lower(get_country_code(centroid));
185
186     NEW.partition := get_partition(NEW.country_code);
187     NEW.geometry_sector := geometry_sector(NEW.partition, centroid);
188   END IF;
189
190   RETURN NEW;
191 END;
192 $$
193 LANGUAGE plpgsql;
194
195
196 CREATE OR REPLACE FUNCTION osmline_update()
197   RETURNS TRIGGER
198   AS $$
199 DECLARE
200   waynodes BIGINT[];
201   prevnode RECORD;
202   nextnode RECORD;
203   startnumber INTEGER;
204   endnumber INTEGER;
205   newstart INTEGER;
206   newend INTEGER;
207   moddiff SMALLINT;
208   linegeo GEOMETRY;
209   splitpoint FLOAT;
210   sectiongeo GEOMETRY;
211   postcode TEXT;
212   stepmod SMALLINT;
213 BEGIN
214   -- deferred delete
215   IF OLD.indexed_status = 100 THEN
216     delete from location_property_osmline where place_id = OLD.place_id;
217     RETURN NULL;
218   END IF;
219
220   IF NEW.indexed_status != 0 OR OLD.indexed_status = 0 THEN
221     RETURN NEW;
222   END IF;
223
224   NEW.parent_place_id := get_interpolation_parent(NEW.token_info, NEW.partition,
225                                                   get_center_point(NEW.linegeo),
226                                                   NEW.linegeo);
227
228   NEW.token_info := token_strip_info(NEW.token_info);
229   IF NEW.address ? '_inherited' THEN
230     NEW.address := NULL;
231   END IF;
232
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
236       NEW.step := 2;
237       stepmod := CASE WHEN NEW.type = 'odd' THEN 1 ELSE 0 END;
238     ELSE
239       NEW.step := CASE WHEN NEW.type = 'all' THEN 1 ELSE (NEW.type)::SMALLINT END;
240       stepmod := NULL;
241     END IF;
242
243     SELECT nodes INTO waynodes FROM place_interpolation WHERE osm_id = NEW.osm_id;
244
245     IF array_upper(waynodes, 1) IS NULL THEN
246       RETURN NEW;
247     END IF;
248
249     linegeo := null;
250     SELECT null::integer as hnr INTO prevnode;
251
252     -- Go through all nodes on the interpolation line that have a housenumber.
253     FOR nextnode IN
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
259           -- variant.
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
267         ORDER BY nodeidpos
268     LOOP
269       {% if debug %}RAISE WARNING 'processing point % (%)', nextnode.hnr, ST_AsText(nextnode.geometry);{% endif %}
270       IF linegeo is null THEN
271         linegeo := NEW.linegeo;
272       ELSE
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.
277           sectiongeo := NULL;
278         ELSEIF splitpoint = 1 THEN
279           -- Point is at the end of the line.
280           sectiongeo := linegeo;
281           linegeo := NULL;
282         ELSE
283           -- Split the line.
284           sectiongeo := ST_LineSubstring(linegeo, 0, splitpoint);
285           linegeo := ST_LineSubstring(linegeo, splitpoint, 1);
286         END IF;
287       END IF;
288
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'
297       THEN
298         IF prevnode.hnr < nextnode.hnr THEN
299           startnumber := prevnode.hnr;
300           endnumber := nextnode.hnr;
301         ELSE
302           startnumber := nextnode.hnr;
303           endnumber := prevnode.hnr;
304           sectiongeo := ST_Reverse(sectiongeo);
305         END IF;
306
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;
311         ELSE
312           newstart := startnumber + 1;
313           moddiff := newstart % NEW.step - stepmod;
314           IF moddiff < 0 THEN
315             newstart := newstart + (NEW.step + moddiff);
316           ELSE
317             newstart := newstart + moddiff;
318           END IF;
319         END IF;
320         newend := newstart + ((endnumber - 1 - newstart) / NEW.step) * NEW.step;
321
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;
327         endnumber := newend;
328
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;
334         END IF;
335         IF postcode is NULL THEN
336             postcode := get_nearest_postcode(NEW.country_code, nextnode.geometry);
337         END IF;
338
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;
347         ELSE
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);
358         END IF;
359       END IF;
360
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
364           RETURN NEW;
365       END IF;
366
367       prevnode := nextnode;
368     END LOOP;
369   END IF;
370
371   RETURN NEW;
372 END;
373 $$
374 LANGUAGE plpgsql;