]> git.openstreetmap.org Git - nominatim.git/blob - lib-sql/functions/interpolation.sql
Merge remote-tracking branch 'upstream/master'
[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) 2022 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
11 CREATE OR REPLACE FUNCTION get_interpolation_address(in_address HSTORE, wayid BIGINT)
12 RETURNS HSTORE
13   AS $$
14 DECLARE
15   location RECORD;
16   waynodes BIGINT[];
17 BEGIN
18   IF in_address ? 'street' or in_address ? 'place' THEN
19     RETURN in_address;
20   END IF;
21
22   SELECT nodes INTO waynodes FROM planet_osm_ways WHERE id = wayid;
23   FOR location IN
24     SELECT placex.address, placex.osm_id FROM placex
25      WHERE osm_type = 'N' and osm_id = ANY(waynodes)
26            and placex.address is not null
27            and (placex.address ? 'street' or placex.address ? 'place')
28            and indexed_status < 100
29   LOOP
30     -- mark it as a derived address
31     RETURN location.address || in_address || hstore('_inherited', '');
32   END LOOP;
33
34   RETURN in_address;
35 END;
36 $$
37 LANGUAGE plpgsql STABLE;
38
39
40
41 -- find the parent road of the cut road parts
42 CREATE OR REPLACE FUNCTION get_interpolation_parent(token_info JSONB,
43                                                     partition SMALLINT,
44                                                     centroid GEOMETRY, geom GEOMETRY)
45   RETURNS BIGINT
46   AS $$
47 DECLARE
48   parent_place_id BIGINT;
49   location RECORD;
50 BEGIN
51   parent_place_id := find_parent_for_address(token_info, partition, centroid);
52
53   IF parent_place_id is null THEN
54     FOR location IN SELECT place_id FROM placex
55         WHERE ST_DWithin(geom, placex.geometry, 0.001)
56               and placex.rank_search = 26
57               and placex.osm_type = 'W' -- needed for index selection
58         ORDER BY CASE WHEN ST_GeometryType(geom) = 'ST_Line' THEN
59                   (ST_distance(placex.geometry, ST_LineInterpolatePoint(geom,0))+
60                   ST_distance(placex.geometry, ST_LineInterpolatePoint(geom,0.5))+
61                   ST_distance(placex.geometry, ST_LineInterpolatePoint(geom,1)))
62                  ELSE ST_distance(placex.geometry, geom) END
63               ASC
64         LIMIT 1
65     LOOP
66       parent_place_id := location.place_id;
67     END LOOP;
68   END IF;
69
70   RETURN parent_place_id;
71 END;
72 $$
73 LANGUAGE plpgsql STABLE;
74
75
76 CREATE OR REPLACE FUNCTION reinsert_interpolation(way_id BIGINT, addr HSTORE,
77                                                   geom GEOMETRY)
78   RETURNS INT
79   AS $$
80 DECLARE
81   existing BIGINT[];
82 BEGIN
83   IF addr is NULL OR NOT addr ? 'interpolation'
84          OR NOT (addr->'interpolation' in ('odd', 'even', 'all')
85                  or addr->'interpolation' similar to '[1-9]')
86   THEN
87     -- the new interpolation is illegal, simply remove existing entries
88     DELETE FROM location_property_osmline WHERE osm_id = way_id;
89   ELSE
90     -- Get the existing entry from the interpolation table.
91     SELECT array_agg(place_id) INTO existing
92       FROM location_property_osmline WHERE osm_id = way_id;
93
94     IF existing IS NULL or array_length(existing, 1) = 0 THEN
95       INSERT INTO location_property_osmline (osm_id, address, linegeo)
96         VALUES (way_id, addr, geom);
97     ELSE
98       -- Update the interpolation table:
99       --   The first entry gets the original data, all other entries
100       --   are removed and will be recreated on indexing.
101       --   (An interpolation can be split up, if it has more than 2 address nodes)
102       UPDATE location_property_osmline
103         SET address = addr,
104             linegeo = geom,
105             startnumber = null,
106             indexed_status = 1
107         WHERE place_id = existing[1];
108       IF array_length(existing, 1) > 1 THEN
109         DELETE FROM location_property_osmline
110           WHERE place_id = any(existing[2:]);
111       END IF;
112     END IF;
113   END IF;
114
115   RETURN 1;
116 END;
117 $$
118 LANGUAGE plpgsql;
119
120
121 CREATE OR REPLACE FUNCTION osmline_insert()
122   RETURNS TRIGGER
123   AS $$
124 DECLARE
125   centroid GEOMETRY;
126 BEGIN
127   NEW.place_id := nextval('seq_place');
128   NEW.indexed_date := now();
129
130   IF NEW.indexed_status IS NULL THEN
131       IF NEW.address is NULL OR NOT NEW.address ? 'interpolation'
132          OR NOT (NEW.address->'interpolation' in ('odd', 'even', 'all')
133                  or NEW.address->'interpolation' similar to '[1-9]')
134       THEN
135           -- alphabetic interpolation is not supported
136           RETURN NULL;
137       END IF;
138
139       NEW.indexed_status := 1; --STATUS_NEW
140       centroid := get_center_point(NEW.linegeo);
141       NEW.country_code := lower(get_country_code(centroid));
142
143       NEW.partition := get_partition(NEW.country_code);
144       NEW.geometry_sector := geometry_sector(NEW.partition, centroid);
145   END IF;
146
147   RETURN NEW;
148 END;
149 $$
150 LANGUAGE plpgsql;
151
152
153 CREATE OR REPLACE FUNCTION osmline_update()
154   RETURNS TRIGGER
155   AS $$
156 DECLARE
157   waynodes BIGINT[];
158   prevnode RECORD;
159   nextnode RECORD;
160   startnumber INTEGER;
161   endnumber INTEGER;
162   newstart INTEGER;
163   newend INTEGER;
164   moddiff SMALLINT;
165   linegeo GEOMETRY;
166   splitpoint FLOAT;
167   sectiongeo GEOMETRY;
168   postcode TEXT;
169   stepmod SMALLINT;
170 BEGIN
171   -- deferred delete
172   IF OLD.indexed_status = 100 THEN
173     delete from location_property_osmline where place_id = OLD.place_id;
174     RETURN NULL;
175   END IF;
176
177   IF NEW.indexed_status != 0 OR OLD.indexed_status = 0 THEN
178     RETURN NEW;
179   END IF;
180
181   NEW.parent_place_id := get_interpolation_parent(NEW.token_info, NEW.partition,
182                                                   get_center_point(NEW.linegeo),
183                                                   NEW.linegeo);
184
185   -- Cannot find a parent street. We will not be able to display a reliable
186   -- address, so drop entire interpolation.
187   IF NEW.parent_place_id is NULL THEN
188     DELETE FROM location_property_osmline where place_id = OLD.place_id;
189     RETURN NULL;
190   END IF;
191
192   NEW.token_info := token_strip_info(NEW.token_info);
193   IF NEW.address ? '_inherited' THEN
194     NEW.address := hstore('interpolation', NEW.address->'interpolation');
195   END IF;
196
197   -- If the line was newly inserted, split the line as necessary.
198   IF OLD.indexed_status = 1 THEN
199     IF NEW.address->'interpolation' in ('odd', 'even') THEN
200       NEW.step := 2;
201       stepmod := CASE WHEN NEW.address->'interpolation' = 'odd' THEN 1 ELSE 0 END;
202     ELSE
203       NEW.step := CASE WHEN NEW.address->'interpolation' = 'all'
204                        THEN 1
205                        ELSE (NEW.address->'interpolation')::SMALLINT END;
206       stepmod := NULL;
207     END IF;
208
209     SELECT nodes INTO waynodes
210       FROM planet_osm_ways WHERE id = NEW.osm_id;
211
212     IF array_upper(waynodes, 1) IS NULL THEN
213       RETURN NEW;
214     END IF;
215
216     linegeo := null;
217     SELECT null::integer as hnr INTO prevnode;
218
219     -- Go through all nodes on the interpolation line that have a housenumber.
220     FOR nextnode IN
221       SELECT DISTINCT ON (nodeidpos)
222           osm_id, address, geometry,
223           -- Take the postcode from the node only if it has a housenumber itself.
224           -- Note that there is a corner-case where the node has a wrongly
225           -- formatted postcode and therefore 'postcode' contains a derived
226           -- variant.
227           CASE WHEN address ? 'postcode' THEN placex.postcode ELSE NULL::text END as postcode,
228           (address->'housenumber')::integer as hnr
229         FROM placex, generate_series(1, array_upper(waynodes, 1)) nodeidpos
230         WHERE osm_type = 'N' and osm_id = waynodes[nodeidpos]::BIGINT
231               and address is not NULL and address ? 'housenumber'
232               and address->'housenumber' ~ '^[0-9]{1,6}$'
233               and ST_Distance(NEW.linegeo, geometry) < 0.0005
234         ORDER BY nodeidpos
235     LOOP
236       {% if debug %}RAISE WARNING 'processing point % (%)', nextnode.hnr, ST_AsText(nextnode.geometry);{% endif %}
237       IF linegeo is null THEN
238         linegeo := NEW.linegeo;
239       ELSE
240         splitpoint := ST_LineLocatePoint(linegeo, nextnode.geometry);
241         IF splitpoint = 0 THEN
242           -- Corner case where the splitpoint falls on the first point
243           -- and thus would not return a geometry. Skip that section.
244           sectiongeo := NULL;
245         ELSEIF splitpoint = 1 THEN
246           -- Point is at the end of the line.
247           sectiongeo := linegeo;
248           linegeo := NULL;
249         ELSE
250           -- Split the line.
251           sectiongeo := ST_LineSubstring(linegeo, 0, splitpoint);
252           linegeo := ST_LineSubstring(linegeo, splitpoint, 1);
253         END IF;
254       END IF;
255
256       IF prevnode.hnr is not null
257          -- Check if there are housenumbers to interpolate between the
258          -- regularly mapped housenumbers.
259          -- (Conveniently also fails if one of the house numbers is not a number.)
260          and abs(prevnode.hnr - nextnode.hnr) > NEW.step
261          -- If the interpolation geometry is broken or two nodes are at the
262          -- same place, then splitting might produce a point. Ignore that.
263          and ST_GeometryType(sectiongeo) = 'ST_LineString'
264       THEN
265         IF prevnode.hnr < nextnode.hnr THEN
266           startnumber := prevnode.hnr;
267           endnumber := nextnode.hnr;
268         ELSE
269           startnumber := nextnode.hnr;
270           endnumber := prevnode.hnr;
271           sectiongeo := ST_Reverse(sectiongeo);
272         END IF;
273
274         -- Adjust the interpolation, so that only inner housenumbers
275         -- are taken into account.
276         IF stepmod is null THEN
277           newstart := startnumber + NEW.step;
278         ELSE
279           newstart := startnumber + 1;
280           moddiff := newstart % NEW.step - stepmod;
281           IF moddiff < 0 THEN
282             newstart := newstart + (NEW.step + moddiff);
283           ELSE
284             newstart := newstart + moddiff;
285           END IF;
286         END IF;
287         newend := newstart + ((endnumber - 1 - newstart) / NEW.step) * NEW.step;
288
289         -- If newstart and newend are the same, then this returns a point.
290         sectiongeo := ST_LineSubstring(sectiongeo,
291                               (newstart - startnumber)::float / (endnumber - startnumber)::float,
292                               (newend - startnumber)::float / (endnumber - startnumber)::float);
293         startnumber := newstart;
294         endnumber := newend;
295
296         -- determine postcode
297         postcode := coalesce(prevnode.postcode, nextnode.postcode, postcode);
298         IF postcode is NULL and NEW.parent_place_id > 0 THEN
299             SELECT placex.postcode FROM placex
300               WHERE place_id = NEW.parent_place_id INTO postcode;
301         END IF;
302         IF postcode is NULL THEN
303             postcode := get_nearest_postcode(NEW.country_code, nextnode.geometry);
304         END IF;
305
306         -- Add the interpolation. If this is the first segment, just modify
307         -- the interpolation to be inserted, otherwise add an additional one
308         -- (marking it indexed already).
309         IF NEW.startnumber IS NULL THEN
310             NEW.startnumber := startnumber;
311             NEW.endnumber := endnumber;
312             NEW.linegeo := sectiongeo;
313             NEW.postcode := postcode;
314         ELSE
315           INSERT INTO location_property_osmline
316                  (linegeo, partition, osm_id, parent_place_id,
317                   startnumber, endnumber, step,
318                   address, postcode, country_code,
319                   geometry_sector, indexed_status)
320           VALUES (sectiongeo, NEW.partition, NEW.osm_id, NEW.parent_place_id,
321                   startnumber, endnumber, NEW.step,
322                   NEW.address, postcode,
323                   NEW.country_code, NEW.geometry_sector, 0);
324         END IF;
325       END IF;
326
327       -- early break if we are out of line string,
328       -- might happen when a line string loops back on itself
329       IF linegeo is null or ST_GeometryType(linegeo) != 'ST_LineString' THEN
330           RETURN NEW;
331       END IF;
332
333       prevnode := nextnode;
334     END LOOP;
335   END IF;
336
337   RETURN NEW;
338 END;
339 $$
340 LANGUAGE plpgsql;