]> git.openstreetmap.org Git - nominatim.git/blob - lib-sql/functions/utils.sql
Merge remote-tracking branch 'upstream/master'
[nominatim.git] / lib-sql / functions / utils.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 -- Assorted helper functions for the triggers.
9
10 CREATE OR REPLACE FUNCTION geometry_sector(partition INTEGER, place geometry)
11   RETURNS INTEGER
12   AS $$
13 DECLARE
14   NEWgeometry geometry;
15 BEGIN
16 --  RAISE WARNING '%',place;
17   NEWgeometry := ST_PointOnSurface(place);
18   RETURN (partition*1000000) + (500-ST_X(NEWgeometry)::integer)*1000 + (500-ST_Y(NEWgeometry)::integer);
19 END;
20 $$
21 LANGUAGE plpgsql IMMUTABLE;
22
23
24 CREATE OR REPLACE FUNCTION array_merge(a INTEGER[], b INTEGER[])
25   RETURNS INTEGER[]
26   AS $$
27 DECLARE
28   i INTEGER;
29   r INTEGER[];
30 BEGIN
31   IF array_upper(a, 1) IS NULL THEN
32     RETURN b;
33   END IF;
34   IF array_upper(b, 1) IS NULL THEN
35     RETURN a;
36   END IF;
37   r := a;
38   FOR i IN 1..array_upper(b, 1) LOOP  
39     IF NOT (ARRAY[b[i]] <@ r) THEN
40       r := r || b[i];
41     END IF;
42   END LOOP;
43   RETURN r;
44 END;
45 $$
46 LANGUAGE plpgsql IMMUTABLE;
47
48 -- Return the node members with a given label from a relation member list
49 -- as a set.
50 --
51 -- \param members      Member list in osm2pgsql middle format.
52 -- \param memberLabels Array of labels to accept.
53 --
54 -- \returns Set of OSM ids of nodes that are found.
55 --
56 CREATE OR REPLACE FUNCTION get_rel_node_members(members TEXT[],
57                                                 memberLabels TEXT[])
58   RETURNS SETOF BIGINT
59   AS $$
60 DECLARE
61   i INTEGER;
62 BEGIN
63   FOR i IN 1..ARRAY_UPPER(members,1) BY 2 LOOP
64     IF members[i+1] = ANY(memberLabels)
65        AND upper(substring(members[i], 1, 1))::char(1) = 'N'
66     THEN
67       RETURN NEXT substring(members[i], 2)::bigint;
68     END IF;
69   END LOOP;
70
71   RETURN;
72 END;
73 $$
74 LANGUAGE plpgsql IMMUTABLE;
75
76
77 CREATE OR REPLACE FUNCTION get_rel_node_members(members JSONB, memberLabels TEXT[])
78   RETURNS SETOF BIGINT
79   AS $$
80 DECLARE
81   member JSONB;
82 BEGIN
83   FOR member IN SELECT * FROM jsonb_array_elements(members)
84   LOOP
85     IF member->>'type' = 'N' and member->>'role' = ANY(memberLabels) THEN
86         RETURN NEXT (member->>'ref')::bigint;
87     END IF;
88   END LOOP;
89
90   RETURN;
91 END;
92 $$
93 LANGUAGE plpgsql IMMUTABLE;
94
95
96 -- Copy 'name' to or from the default language.
97 --
98 -- \param country_code     Country code of the object being named.
99 -- \param[inout] name      List of names of the object.
100 --
101 -- If the country named by country_code has a single default language,
102 -- then a `name` tag is copied to `name:<country_code>` if this tag does
103 -- not yet exist and vice versa.
104 CREATE OR REPLACE FUNCTION add_default_place_name(country_code VARCHAR(2),
105                                                   INOUT name HSTORE)
106   AS $$
107 DECLARE
108   default_language VARCHAR(10);
109 BEGIN
110   IF name is not null AND array_upper(akeys(name),1) > 1 THEN
111     default_language := get_country_language_code(country_code);
112     IF default_language IS NOT NULL THEN
113       IF name ? 'name' AND NOT name ? ('name:'||default_language) THEN
114         name := name || hstore(('name:'||default_language), (name -> 'name'));
115       ELSEIF name ? ('name:'||default_language) AND NOT name ? 'name' THEN
116         name := name || hstore('name', (name -> ('name:'||default_language)));
117       END IF;
118     END IF;
119   END IF;
120 END;
121 $$
122 LANGUAGE plpgsql IMMUTABLE;
123
124
125 -- Find the nearest artificial postcode for the given geometry.
126 -- TODO For areas there should not be more than two inside the geometry.
127 CREATE OR REPLACE FUNCTION get_nearest_postcode(country VARCHAR(2), geom GEOMETRY)
128   RETURNS TEXT
129   AS $$
130 DECLARE
131   outcode TEXT;
132   cnt INTEGER;
133 BEGIN
134     -- If the geometry is an area then only one postcode must be within
135     -- that area, otherwise consider the area as not having a postcode.
136     IF ST_GeometryType(geom) in ('ST_Polygon','ST_MultiPolygon') THEN
137         SELECT min(postcode), count(*) FROM
138               (SELECT postcode FROM location_postcode
139                 WHERE ST_Contains(geom, location_postcode.geometry) LIMIT 2) sub
140           INTO outcode, cnt;
141
142         IF cnt = 1 THEN
143             RETURN outcode;
144         ELSE
145             RETURN null;
146         END IF;
147     END IF;
148
149     SELECT postcode FROM location_postcode
150      WHERE ST_DWithin(geom, location_postcode.geometry, 0.05)
151           AND location_postcode.country_code = country
152      ORDER BY ST_Distance(geom, location_postcode.geometry) LIMIT 1
153     INTO outcode;
154
155     RETURN outcode;
156 END;
157 $$
158 LANGUAGE plpgsql STABLE;
159
160
161 CREATE OR REPLACE FUNCTION get_country_code(place geometry)
162   RETURNS TEXT
163   AS $$
164 DECLARE
165   place_centre GEOMETRY;
166   nearcountry RECORD;
167   countries TEXT[];
168 BEGIN
169   place_centre := ST_PointOnSurface(place);
170
171 -- RAISE WARNING 'get_country_code, start: %', ST_AsText(place_centre);
172
173   -- Try for a OSM polygon
174   SELECT array_agg(country_code) FROM location_area_country
175     WHERE country_code is not null and st_covers(geometry, place_centre)
176     INTO countries;
177
178   IF array_length(countries, 1) = 1 THEN
179     RETURN countries[1];
180   END IF;
181
182   IF array_length(countries, 1) > 1 THEN
183     -- more than one country found, confirm against the fallback data what to choose
184     FOR nearcountry IN
185         SELECT country_code FROM country_osm_grid
186           WHERE ST_Covers(geometry, place_centre) AND country_code = ANY(countries)
187           ORDER BY area ASC
188     LOOP
189         RETURN nearcountry.country_code;
190     END LOOP;
191     -- Still nothing? Choose the country code with the smallest partition number.
192     -- And failing that, just go by the alphabet.
193     FOR nearcountry IN
194         SELECT cc,
195                (SELECT partition FROM country_name WHERE country_code = cc) as partition
196         FROM unnest(countries) cc
197         ORDER BY partition, cc
198     LOOP
199         RETURN nearcountry.cc;
200     END LOOP;
201
202     -- Should never be reached.
203     RETURN countries[1];
204   END IF;
205
206 -- RAISE WARNING 'osm fallback: %', ST_AsText(place_centre);
207
208   -- Try for OSM fallback data
209   -- The order is to deal with places like HongKong that are 'states' within another polygon
210   FOR nearcountry IN
211     SELECT country_code from country_osm_grid
212     WHERE st_covers(geometry, place_centre) order by area asc limit 1
213   LOOP
214     RETURN nearcountry.country_code;
215   END LOOP;
216
217 -- RAISE WARNING 'near osm fallback: %', ST_AsText(place_centre);
218
219   RETURN NULL;
220 END;
221 $$
222 LANGUAGE plpgsql STABLE;
223
224
225 CREATE OR REPLACE FUNCTION get_country_language_code(search_country_code VARCHAR(2))
226   RETURNS TEXT
227   AS $$
228 DECLARE
229   nearcountry RECORD;
230 BEGIN
231   FOR nearcountry IN
232     SELECT distinct country_default_language_code from country_name
233     WHERE country_code = search_country_code limit 1
234   LOOP
235     RETURN lower(nearcountry.country_default_language_code);
236   END LOOP;
237   RETURN NULL;
238 END;
239 $$
240 LANGUAGE plpgsql STABLE;
241
242
243 CREATE OR REPLACE FUNCTION get_partition(in_country_code VARCHAR(10))
244   RETURNS INTEGER
245   AS $$
246 DECLARE
247   nearcountry RECORD;
248 BEGIN
249   FOR nearcountry IN
250     SELECT partition from country_name where country_code = in_country_code
251   LOOP
252     RETURN nearcountry.partition;
253   END LOOP;
254   RETURN 0;
255 END;
256 $$
257 LANGUAGE plpgsql STABLE;
258
259
260 -- Find the parent of an address with addr:street/addr:place tag.
261 --
262 -- \param token_info Naming info with the address information.
263 -- \param partition  Partition where to search the parent.
264 -- \param centroid   Location of the address.
265 --
266 -- \return Place ID of the parent if one was found, NULL otherwise.
267 CREATE OR REPLACE FUNCTION find_parent_for_address(token_info JSONB,
268                                                    partition SMALLINT,
269                                                    centroid GEOMETRY)
270   RETURNS BIGINT
271   AS $$
272 DECLARE
273   parent_place_id BIGINT;
274 BEGIN
275   -- Check for addr:street attributes
276   parent_place_id := getNearestNamedRoadPlaceId(partition, centroid, token_info);
277   IF parent_place_id is not null THEN
278     {% if debug %}RAISE WARNING 'Get parent from addr:street: %', parent_place_id;{% endif %}
279     RETURN parent_place_id;
280   END IF;
281
282   -- Check for addr:place attributes.
283   parent_place_id := getNearestNamedPlacePlaceId(partition, centroid, token_info);
284   {% if debug %}RAISE WARNING 'Get parent from addr:place: %', parent_place_id;{% endif %}
285   RETURN parent_place_id;
286 END;
287 $$
288 LANGUAGE plpgsql STABLE;
289
290
291 CREATE OR REPLACE FUNCTION delete_location(OLD_place_id BIGINT)
292   RETURNS BOOLEAN
293   AS $$
294 DECLARE
295 BEGIN
296   DELETE FROM location_area where place_id = OLD_place_id;
297 -- TODO:location_area
298   RETURN true;
299 END;
300 $$
301 LANGUAGE plpgsql;
302
303 -- Create a bounding box with an extent computed from the radius (in meters)
304 -- which in turn is derived from the given search rank.
305 CREATE OR REPLACE FUNCTION place_node_fuzzy_area(geom GEOMETRY, rank_search INTEGER)
306   RETURNS GEOMETRY
307   AS $$
308 DECLARE
309   radius FLOAT := 500;
310 BEGIN
311   IF rank_search <= 16 THEN -- city
312     radius := 15000;
313   ELSIF rank_search <= 18 THEN -- town
314     radius := 4000;
315   ELSIF rank_search <= 19 THEN -- village
316     radius := 2000;
317   ELSIF rank_search  <= 20 THEN -- hamlet
318     radius := 1000;
319   END IF;
320
321   RETURN ST_Envelope(ST_Collect(
322                      ST_Project(geom::geography, radius, 0.785398)::geometry,
323                      ST_Project(geom::geography, radius, 3.9269908)::geometry));
324 END;
325 $$
326 LANGUAGE plpgsql IMMUTABLE;
327
328
329 CREATE OR REPLACE FUNCTION add_location(place_id BIGINT, country_code varchar(2),
330                                         partition INTEGER, keywords INTEGER[],
331                                         rank_search INTEGER, rank_address INTEGER,
332                                         in_postcode TEXT, geometry GEOMETRY,
333                                         centroid GEOMETRY)
334   RETURNS BOOLEAN
335   AS $$
336 DECLARE
337   locationid INTEGER;
338   secgeo GEOMETRY;
339   postcode TEXT;
340 BEGIN
341   PERFORM deleteLocationArea(partition, place_id, rank_search);
342
343   -- add postcode only if it contains a single entry, i.e. ignore postcode lists
344   postcode := NULL;
345   IF in_postcode is not null AND in_postcode not similar to '%(,|;)%' THEN
346       postcode := upper(trim (in_postcode));
347   END IF;
348
349   IF ST_GeometryType(geometry) in ('ST_Polygon','ST_MultiPolygon') THEN
350     FOR secgeo IN select split_geometry(geometry) AS geom LOOP
351       PERFORM insertLocationAreaLarge(partition, place_id, country_code, keywords, rank_search, rank_address, false, postcode, centroid, secgeo);
352     END LOOP;
353
354   ELSEIF ST_GeometryType(geometry) = 'ST_Point' THEN
355     secgeo := place_node_fuzzy_area(geometry, rank_search);
356     PERFORM insertLocationAreaLarge(partition, place_id, country_code, keywords, rank_search, rank_address, true, postcode, centroid, secgeo);
357
358   END IF;
359
360   RETURN true;
361 END;
362 $$
363 LANGUAGE plpgsql;
364
365
366 CREATE OR REPLACE FUNCTION quad_split_geometry(geometry GEOMETRY, maxarea FLOAT,
367                                                maxdepth INTEGER)
368   RETURNS SETOF GEOMETRY
369   AS $$
370 DECLARE
371   xmin FLOAT;
372   ymin FLOAT;
373   xmax FLOAT;
374   ymax FLOAT;
375   xmid FLOAT;
376   ymid FLOAT;
377   secgeo GEOMETRY;
378   secbox GEOMETRY;
379   seg INTEGER;
380   geo RECORD;
381   area FLOAT;
382   remainingdepth INTEGER;
383   added INTEGER;
384 BEGIN
385
386 --  RAISE WARNING 'quad_split_geometry: maxarea=%, depth=%',maxarea,maxdepth;
387
388   IF (ST_GeometryType(geometry) not in ('ST_Polygon','ST_MultiPolygon') OR NOT ST_IsValid(geometry)) THEN
389     RETURN NEXT geometry;
390     RETURN;
391   END IF;
392
393   remainingdepth := maxdepth - 1;
394   area := ST_AREA(geometry);
395   IF remainingdepth < 1 OR area < maxarea THEN
396     RETURN NEXT geometry;
397     RETURN;
398   END IF;
399
400   xmin := st_xmin(geometry);
401   xmax := st_xmax(geometry);
402   ymin := st_ymin(geometry);
403   ymax := st_ymax(geometry);
404   secbox := ST_SetSRID(ST_MakeBox2D(ST_Point(ymin,xmin),ST_Point(ymax,xmax)),4326);
405
406   -- if the geometry completely covers the box don't bother to slice any more
407   IF ST_AREA(secbox) = area THEN
408     RETURN NEXT geometry;
409     RETURN;
410   END IF;
411
412   xmid := (xmin+xmax)/2;
413   ymid := (ymin+ymax)/2;
414
415   added := 0;
416   FOR seg IN 1..4 LOOP
417
418     IF seg = 1 THEN
419       secbox := ST_SetSRID(ST_MakeBox2D(ST_Point(xmin,ymin),ST_Point(xmid,ymid)),4326);
420     END IF;
421     IF seg = 2 THEN
422       secbox := ST_SetSRID(ST_MakeBox2D(ST_Point(xmin,ymid),ST_Point(xmid,ymax)),4326);
423     END IF;
424     IF seg = 3 THEN
425       secbox := ST_SetSRID(ST_MakeBox2D(ST_Point(xmid,ymin),ST_Point(xmax,ymid)),4326);
426     END IF;
427     IF seg = 4 THEN
428       secbox := ST_SetSRID(ST_MakeBox2D(ST_Point(xmid,ymid),ST_Point(xmax,ymax)),4326);
429     END IF;
430
431     IF st_intersects(geometry, secbox) THEN
432       secgeo := st_intersection(geometry, secbox);
433       IF NOT ST_IsEmpty(secgeo) AND ST_GeometryType(secgeo) in ('ST_Polygon','ST_MultiPolygon') THEN
434         FOR geo IN select quad_split_geometry(secgeo, maxarea, remainingdepth) as geom LOOP
435           IF NOT ST_IsEmpty(geo.geom) AND ST_GeometryType(geo.geom) in ('ST_Polygon','ST_MultiPolygon') THEN
436             added := added + 1;
437             RETURN NEXT geo.geom;
438           END IF;
439         END LOOP;
440       END IF;
441     END IF;
442   END LOOP;
443
444   RETURN;
445 END;
446 $$
447 LANGUAGE plpgsql IMMUTABLE;
448
449
450 CREATE OR REPLACE FUNCTION split_geometry(geometry GEOMETRY)
451   RETURNS SETOF GEOMETRY
452   AS $$
453 DECLARE
454   geo RECORD;
455 BEGIN
456   -- 10000000000 is ~~ 1x1 degree
457   FOR geo IN select quad_split_geometry(geometry, 0.25, 20) as geom LOOP
458     RETURN NEXT geo.geom;
459   END LOOP;
460   RETURN;
461 END;
462 $$
463 LANGUAGE plpgsql IMMUTABLE;
464
465 CREATE OR REPLACE FUNCTION simplify_large_polygons(geometry GEOMETRY)
466   RETURNS GEOMETRY
467   AS $$
468 BEGIN
469   IF ST_GeometryType(geometry) in ('ST_Polygon','ST_MultiPolygon')
470      and ST_MemSize(geometry) > 3000000
471   THEN
472     geometry := ST_SimplifyPreserveTopology(geometry, 0.0001);
473   END IF;
474   RETURN geometry;
475 END;
476 $$
477 LANGUAGE plpgsql IMMUTABLE;
478
479
480 CREATE OR REPLACE FUNCTION place_force_delete(placeid BIGINT)
481   RETURNS BOOLEAN
482   AS $$
483 DECLARE
484     osmid BIGINT;
485     osmtype character(1);
486     pclass text;
487     ptype text;
488 BEGIN
489   SELECT osm_type, osm_id, class, type FROM placex WHERE place_id = placeid INTO osmtype, osmid, pclass, ptype;
490   DELETE FROM import_polygon_delete where osm_type = osmtype and osm_id = osmid and class = pclass and type = ptype;
491   DELETE FROM import_polygon_error where osm_type = osmtype and osm_id = osmid and class = pclass and type = ptype;
492   -- force delete by directly entering it into the to-be-deleted table
493   INSERT INTO place_to_be_deleted (osm_type, osm_id, class, type, deferred)
494          VALUES(osmtype, osmid, pclass, ptype, false);
495   PERFORM flush_deleted_places();
496
497   RETURN TRUE;
498 END;
499 $$
500 LANGUAGE plpgsql;
501
502
503 CREATE OR REPLACE FUNCTION place_force_update(placeid BIGINT)
504   RETURNS BOOLEAN
505   AS $$
506 DECLARE
507   placegeom GEOMETRY;
508   geom GEOMETRY;
509   diameter FLOAT;
510   rank SMALLINT;
511 BEGIN
512   UPDATE placex SET indexed_status = 2 WHERE place_id = placeid;
513
514   SELECT geometry, rank_address INTO placegeom, rank
515     FROM placex WHERE place_id = placeid;
516
517   IF placegeom IS NOT NULL AND ST_IsValid(placegeom) THEN
518     IF ST_GeometryType(placegeom) in ('ST_Polygon','ST_MultiPolygon')
519        AND rank > 0
520     THEN
521       FOR geom IN SELECT split_geometry(placegeom) LOOP
522         UPDATE placex SET indexed_status = 2
523          WHERE ST_Intersects(geom, placex.geometry)
524                and indexed_status = 0
525                and ((rank_address = 0 and rank_search > rank) or rank_address > rank)
526                and (rank_search < 28 or name is not null or (rank >= 16 and address ? 'place'));
527       END LOOP;
528     ELSE
529         diameter := update_place_diameter(rank);
530         IF diameter > 0 THEN
531           IF rank >= 26 THEN
532             -- roads may cause reparenting for >27 rank places
533             update placex set indexed_status = 2 where indexed_status = 0 and rank_search > rank and ST_DWithin(placex.geometry, placegeom, diameter);
534           ELSEIF rank >= 16 THEN
535             -- up to rank 16, street-less addresses may need reparenting
536             update placex set indexed_status = 2 where indexed_status = 0 and rank_search > rank and ST_DWithin(placex.geometry, placegeom, diameter) and (rank_search < 28 or name is not null or address ? 'place');
537           ELSE
538             -- for all other places the search terms may change as well
539             update placex set indexed_status = 2 where indexed_status = 0 and rank_search > rank and ST_DWithin(placex.geometry, placegeom, diameter) and (rank_search < 28 or name is not null);
540           END IF;
541         END IF;
542     END IF;
543     RETURN TRUE;
544   END IF;
545
546   RETURN FALSE;
547 END;
548 $$
549 LANGUAGE plpgsql;
550
551 CREATE OR REPLACE FUNCTION flush_deleted_places()
552   RETURNS INTEGER
553   AS $$
554 BEGIN
555   -- deleting large polygons can have a massive effect on the system - require manual intervention to let them through
556   INSERT INTO import_polygon_delete (osm_type, osm_id, class, type)
557     SELECT osm_type, osm_id, class, type FROM place_to_be_deleted WHERE deferred;
558
559   -- delete from place table
560   ALTER TABLE place DISABLE TRIGGER place_before_delete;
561   DELETE FROM place USING place_to_be_deleted
562     WHERE place.osm_type = place_to_be_deleted.osm_type
563           and place.osm_id = place_to_be_deleted.osm_id
564           and place.class = place_to_be_deleted.class
565           and place.type = place_to_be_deleted.type
566           and not deferred;
567   ALTER TABLE place ENABLE TRIGGER place_before_delete;
568
569   -- Mark for delete in the placex table
570   UPDATE placex SET indexed_status = 100 FROM place_to_be_deleted
571     WHERE placex.osm_type = 'N' and place_to_be_deleted.osm_type = 'N'
572           and placex.osm_id = place_to_be_deleted.osm_id
573           and placex.class = place_to_be_deleted.class
574           and placex.type = place_to_be_deleted.type
575           and not deferred;
576   UPDATE placex SET indexed_status = 100 FROM place_to_be_deleted
577     WHERE placex.osm_type = 'W' and place_to_be_deleted.osm_type = 'W'
578           and placex.osm_id = place_to_be_deleted.osm_id
579           and placex.class = place_to_be_deleted.class
580           and placex.type = place_to_be_deleted.type
581           and not deferred;
582   UPDATE placex SET indexed_status = 100 FROM place_to_be_deleted
583     WHERE placex.osm_type = 'R' and place_to_be_deleted.osm_type = 'R'
584           and placex.osm_id = place_to_be_deleted.osm_id
585           and placex.class = place_to_be_deleted.class
586           and placex.type = place_to_be_deleted.type
587           and not deferred;
588
589    -- Mark for delete in interpolations
590    UPDATE location_property_osmline SET indexed_status = 100 FROM place_to_be_deleted
591     WHERE place_to_be_deleted.osm_type = 'W'
592           and place_to_be_deleted.class = 'place'
593           and place_to_be_deleted.type = 'houses'
594           and location_property_osmline.osm_id = place_to_be_deleted.osm_id
595           and not deferred;
596
597    -- Clear todo list.
598    TRUNCATE TABLE place_to_be_deleted;
599
600    RETURN NULL;
601 END;
602 $$ LANGUAGE plpgsql;