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