]> git.openstreetmap.org Git - nominatim.git/blob - lib/Geocode.php
cleanup of SQL for readability. No logic change
[nominatim.git] / lib / Geocode.php
1 <?php
2
3 namespace Nominatim;
4
5 require_once(CONST_BasePath.'/lib/PlaceLookup.php');
6 require_once(CONST_BasePath.'/lib/ReverseGeocode.php');
7
8 class Geocode
9 {
10     protected $oDB;
11
12     protected $aLangPrefOrder = array();
13
14     protected $bIncludeAddressDetails = false;
15     protected $bIncludeExtraTags = false;
16     protected $bIncludeNameDetails = false;
17
18     protected $bIncludePolygonAsPoints = false;
19     protected $bIncludePolygonAsText = false;
20     protected $bIncludePolygonAsGeoJSON = false;
21     protected $bIncludePolygonAsKML = false;
22     protected $bIncludePolygonAsSVG = false;
23     protected $fPolygonSimplificationThreshold = 0.0;
24
25     protected $aExcludePlaceIDs = array();
26     protected $bDeDupe = true;
27     protected $bReverseInPlan = false;
28
29     protected $iLimit = 20;
30     protected $iFinalLimit = 10;
31     protected $iOffset = 0;
32     protected $bFallback = false;
33
34     protected $aCountryCodes = false;
35     protected $aNearPoint = false;
36
37     protected $bBoundedSearch = false;
38     protected $aViewBox = false;
39     protected $sViewboxCentreSQL = false;
40     protected $sViewboxSmallSQL = false;
41     protected $sViewboxLargeSQL = false;
42
43     protected $iMaxRank = 20;
44     protected $iMinAddressRank = 0;
45     protected $iMaxAddressRank = 30;
46     protected $aAddressRankList = array();
47     protected $exactMatchCache = array();
48
49     protected $sAllowedTypesSQLList = false;
50
51     protected $sQuery = false;
52     protected $aStructuredQuery = false;
53
54
55     public function __construct(&$oDB)
56     {
57         $this->oDB =& $oDB;
58     }
59
60     public function setReverseInPlan($bReverse)
61     {
62         $this->bReverseInPlan = $bReverse;
63     }
64
65     public function setLanguagePreference($aLangPref)
66     {
67         $this->aLangPrefOrder = $aLangPref;
68     }
69
70     public function getIncludeAddressDetails()
71     {
72         return $this->bIncludeAddressDetails;
73     }
74
75     public function getIncludeExtraTags()
76     {
77         return $this->bIncludeExtraTags;
78     }
79
80     public function getIncludeNameDetails()
81     {
82         return $this->bIncludeNameDetails;
83     }
84
85     public function setIncludePolygonAsPoints($b = true)
86     {
87         $this->bIncludePolygonAsPoints = $b;
88     }
89
90     public function setIncludePolygonAsText($b = true)
91     {
92         $this->bIncludePolygonAsText = $b;
93     }
94
95     public function setIncludePolygonAsGeoJSON($b = true)
96     {
97         $this->bIncludePolygonAsGeoJSON = $b;
98     }
99
100     public function setIncludePolygonAsKML($b = true)
101     {
102         $this->bIncludePolygonAsKML = $b;
103     }
104
105     public function setIncludePolygonAsSVG($b = true)
106     {
107         $this->bIncludePolygonAsSVG = $b;
108     }
109
110     public function setPolygonSimplificationThreshold($f)
111     {
112         $this->fPolygonSimplificationThreshold = $f;
113     }
114
115     public function setLimit($iLimit = 10)
116     {
117         if ($iLimit > 50) $iLimit = 50;
118         if ($iLimit < 1) $iLimit = 1;
119
120         $this->iFinalLimit = $iLimit;
121         $this->iLimit = $iLimit + min($iLimit, 10);
122     }
123
124     public function getExcludedPlaceIDs()
125     {
126         return $this->aExcludePlaceIDs;
127     }
128
129     public function getViewBoxString()
130     {
131         if (!$this->aViewBox) return null;
132         return $this->aViewBox[0].','.$this->aViewBox[3].','.$this->aViewBox[2].','.$this->aViewBox[1];
133     }
134
135     public function setFeatureType($sFeatureType)
136     {
137         switch ($sFeatureType) {
138             case 'country':
139                 $this->setRankRange(4, 4);
140                 break;
141             case 'state':
142                 $this->setRankRange(8, 8);
143                 break;
144             case 'city':
145                 $this->setRankRange(14, 16);
146                 break;
147             case 'settlement':
148                 $this->setRankRange(8, 20);
149                 break;
150         }
151     }
152
153     public function setRankRange($iMin, $iMax)
154     {
155         $this->iMinAddressRank = $iMin;
156         $this->iMaxAddressRank = $iMax;
157     }
158
159     public function setRoute($aRoutePoints, $fRouteWidth)
160     {
161         $this->aViewBox = false;
162
163         $this->sViewboxCentreSQL = "ST_SetSRID('LINESTRING(";
164         $sSep = '';
165         foreach ($aRoutePoints as $aPoint) {
166             $fPoint = (float)$aPoint;
167             $this->sViewboxCentreSQL .= $sSep.$fPoint;
168             $sSep = ($sSep == ' ') ? ',' : ' ';
169         }
170         $this->sViewboxCentreSQL .= ")'::geometry,4326)";
171
172         $this->sViewboxSmallSQL = 'ST_BUFFER('.$this->sViewboxCentreSQL;
173         $this->sViewboxSmallSQL .= ','.($fRouteWidth/69).')';
174
175         $this->sViewboxLargeSQL = 'ST_BUFFER('.$this->sViewboxCentreSQL;
176         $this->sViewboxLargeSQL .= ','.($fRouteWidth/30).')';
177     }
178
179     public function setViewbox($aViewbox)
180     {
181         $this->aViewBox = array_map('floatval', $aViewbox);
182
183         $this->aViewBox[0] = max(-180.0, min(180, $this->aViewBox[0]));
184         $this->aViewBox[1] = max(-90.0, min(90, $this->aViewBox[1]));
185         $this->aViewBox[2] = max(-180.0, min(180, $this->aViewBox[2]));
186         $this->aViewBox[3] = max(-90.0, min(90, $this->aViewBox[3]));
187
188         if (abs($this->aViewBox[0] - $this->aViewBox[2]) < 0.000000001
189             || abs($this->aViewBox[1] - $this->aViewBox[3]) < 0.000000001
190         ) {
191             userError("Bad parameter 'viewbox'. Not a box.");
192         }
193
194         $fHeight = $this->aViewBox[0] - $this->aViewBox[2];
195         $fWidth = $this->aViewBox[1] - $this->aViewBox[3];
196         $aBigViewBox[0] = $this->aViewBox[0] + $fHeight;
197         $aBigViewBox[2] = $this->aViewBox[2] - $fHeight;
198         $aBigViewBox[1] = $this->aViewBox[1] + $fWidth;
199         $aBigViewBox[3] = $this->aViewBox[3] - $fWidth;
200
201         $this->sViewboxCentreSQL = false;
202         $this->sViewboxSmallSQL = sprintf(
203             'ST_SetSRID(ST_MakeBox2D(ST_Point(%F,%F),ST_Point(%F,%F)),4326)',
204             $this->aViewBox[0],
205             $this->aViewBox[1],
206             $this->aViewBox[2],
207             $this->aViewBox[3]
208         );
209         $this->sViewboxLargeSQL = sprintf(
210             'ST_SetSRID(ST_MakeBox2D(ST_Point(%F,%F),ST_Point(%F,%F)),4326)',
211             $aBigViewBox[0],
212             $aBigViewBox[1],
213             $aBigViewBox[2],
214             $aBigViewBox[3]
215         );
216     }
217
218     public function setNearPoint($aNearPoint, $fRadiusDeg = 0.1)
219     {
220         $this->aNearPoint = array((float)$aNearPoint[0], (float)$aNearPoint[1], (float)$fRadiusDeg);
221     }
222
223     public function setQuery($sQueryString)
224     {
225         $this->sQuery = $sQueryString;
226         $this->aStructuredQuery = false;
227     }
228
229     public function getQueryString()
230     {
231         return $this->sQuery;
232     }
233
234
235     public function loadParamArray($oParams)
236     {
237         $this->bIncludeAddressDetails
238          = $oParams->getBool('addressdetails', $this->bIncludeAddressDetails);
239         $this->bIncludeExtraTags
240          = $oParams->getBool('extratags', $this->bIncludeExtraTags);
241         $this->bIncludeNameDetails
242          = $oParams->getBool('namedetails', $this->bIncludeNameDetails);
243
244         $this->bBoundedSearch = $oParams->getBool('bounded', $this->bBoundedSearch);
245         $this->bDeDupe = $oParams->getBool('dedupe', $this->bDeDupe);
246
247         $this->setLimit($oParams->getInt('limit', $this->iFinalLimit));
248         $this->iOffset = $oParams->getInt('offset', $this->iOffset);
249
250         $this->bFallback = $oParams->getBool('fallback', $this->bFallback);
251
252         // List of excluded Place IDs - used for more acurate pageing
253         $sExcluded = $oParams->getStringList('exclude_place_ids');
254         if ($sExcluded) {
255             foreach ($sExcluded as $iExcludedPlaceID) {
256                 $iExcludedPlaceID = (int)$iExcludedPlaceID;
257                 if ($iExcludedPlaceID)
258                     $aExcludePlaceIDs[$iExcludedPlaceID] = $iExcludedPlaceID;
259             }
260
261             if (isset($aExcludePlaceIDs))
262                 $this->aExcludePlaceIDs = $aExcludePlaceIDs;
263         }
264
265         // Only certain ranks of feature
266         $sFeatureType = $oParams->getString('featureType');
267         if (!$sFeatureType) $sFeatureType = $oParams->getString('featuretype');
268         if ($sFeatureType) $this->setFeatureType($sFeatureType);
269
270         // Country code list
271         $sCountries = $oParams->getStringList('countrycodes');
272         if ($sCountries) {
273             foreach ($sCountries as $sCountryCode) {
274                 if (preg_match('/^[a-zA-Z][a-zA-Z]$/', $sCountryCode)) {
275                     $aCountries[] = strtolower($sCountryCode);
276                 }
277             }
278             if (isset($aCountries))
279                 $this->aCountryCodes = $aCountries;
280         }
281
282         $aViewbox = $oParams->getStringList('viewboxlbrt');
283         if ($aViewbox) {
284             if (count($aViewbox) != 4) {
285                 userError("Bad parmater 'viewbox'. Expected 4 coordinates.");
286             }
287             $this->setViewbox($aViewbox);
288         } else {
289             $aViewbox = $oParams->getStringList('viewbox');
290             if ($aViewbox) {
291                 if (count($aViewbox) != 4) {
292                     userError("Bad parmater 'viewbox'. Expected 4 coordinates.");
293                 }
294                 $this->setViewBox(array(
295                                    $aViewbox[0],
296                                    $aViewbox[3],
297                                    $aViewbox[2],
298                                    $aViewbox[1]
299                                   ));
300             } else {
301                 $aRoute = $oParams->getStringList('route');
302                 $fRouteWidth = $oParams->getFloat('routewidth');
303                 if ($aRoute && $fRouteWidth) {
304                     $this->setRoute($aRoute, $fRouteWidth);
305                 }
306             }
307         }
308     }
309
310     public function setQueryFromParams($oParams)
311     {
312         // Search query
313         $sQuery = $oParams->getString('q');
314         if (!$sQuery) {
315             $this->setStructuredQuery(
316                 $oParams->getString('amenity'),
317                 $oParams->getString('street'),
318                 $oParams->getString('city'),
319                 $oParams->getString('county'),
320                 $oParams->getString('state'),
321                 $oParams->getString('country'),
322                 $oParams->getString('postalcode')
323             );
324             $this->setReverseInPlan(false);
325         } else {
326             $this->setQuery($sQuery);
327         }
328     }
329
330     public function loadStructuredAddressElement($sValue, $sKey, $iNewMinAddressRank, $iNewMaxAddressRank, $aItemListValues)
331     {
332         $sValue = trim($sValue);
333         if (!$sValue) return false;
334         $this->aStructuredQuery[$sKey] = $sValue;
335         if ($this->iMinAddressRank == 0 && $this->iMaxAddressRank == 30) {
336             $this->iMinAddressRank = $iNewMinAddressRank;
337             $this->iMaxAddressRank = $iNewMaxAddressRank;
338         }
339         if ($aItemListValues) $this->aAddressRankList = array_merge($this->aAddressRankList, $aItemListValues);
340         return true;
341     }
342
343     public function setStructuredQuery($sAmentiy = false, $sStreet = false, $sCity = false, $sCounty = false, $sState = false, $sCountry = false, $sPostalCode = false)
344     {
345         $this->sQuery = false;
346
347         // Reset
348         $this->iMinAddressRank = 0;
349         $this->iMaxAddressRank = 30;
350         $this->aAddressRankList = array();
351
352         $this->aStructuredQuery = array();
353         $this->sAllowedTypesSQLList = '';
354
355         $this->loadStructuredAddressElement($sAmentiy, 'amenity', 26, 30, false);
356         $this->loadStructuredAddressElement($sStreet, 'street', 26, 30, false);
357         $this->loadStructuredAddressElement($sCity, 'city', 14, 24, false);
358         $this->loadStructuredAddressElement($sCounty, 'county', 9, 13, false);
359         $this->loadStructuredAddressElement($sState, 'state', 8, 8, false);
360         $this->loadStructuredAddressElement($sPostalCode, 'postalcode', 5, 11, array(5, 11));
361         $this->loadStructuredAddressElement($sCountry, 'country', 4, 4, false);
362
363         if (sizeof($this->aStructuredQuery) > 0) {
364             $this->sQuery = join(', ', $this->aStructuredQuery);
365             if ($this->iMaxAddressRank < 30) {
366                 $sAllowedTypesSQLList = '(\'place\',\'boundary\')';
367             }
368         }
369     }
370
371     public function fallbackStructuredQuery()
372     {
373         if (!$this->aStructuredQuery) return false;
374
375         $aParams = $this->aStructuredQuery;
376
377         if (sizeof($aParams) == 1) return false;
378
379         $aOrderToFallback = array('postalcode', 'street', 'city', 'county', 'state');
380
381         foreach ($aOrderToFallback as $sType) {
382             if (isset($aParams[$sType])) {
383                 unset($aParams[$sType]);
384                 $this->setStructuredQuery(@$aParams['amenity'], @$aParams['street'], @$aParams['city'], @$aParams['county'], @$aParams['state'], @$aParams['country'], @$aParams['postalcode']);
385                 return true;
386             }
387         }
388
389         return false;
390     }
391
392     public function getDetails($aPlaceIDs)
393     {
394         //$aPlaceIDs is an array with key: placeID and value: tiger-housenumber, if found, else -1
395         if (sizeof($aPlaceIDs) == 0) return array();
396
397         $sLanguagePrefArraySQL = "ARRAY[".join(',', array_map("getDBQuoted", $this->aLangPrefOrder))."]";
398
399         // Get the details for display (is this a redundant extra step?)
400         $sPlaceIDs = join(',', array_keys($aPlaceIDs));
401
402         $sImportanceSQL = '';
403         if ($this->sViewboxSmallSQL) $sImportanceSQL .= " CASE WHEN ST_Contains($this->sViewboxSmallSQL, ST_Collect(centroid)) THEN 1 ELSE 0.75 END * ";
404         if ($this->sViewboxLargeSQL) $sImportanceSQL .= " CASE WHEN ST_Contains($this->sViewboxLargeSQL, ST_Collect(centroid)) THEN 1 ELSE 0.75 END * ";
405
406         $sSQL  = "SELECT ";
407         $sSQL .= "    osm_type,";
408         $sSQL .= "    osm_id,";
409         $sSQL .= "    class,";
410         $sSQL .= "    type,";
411         $sSQL .= "    admin_level,";
412         $sSQL .= "    rank_search,";
413         $sSQL .= "    rank_address,";
414         $sSQL .= "    min(place_id) AS place_id, ";
415         $sSQL .= "    min(parent_place_id) AS parent_place_id, ";
416         $sSQL .= "    calculated_country_code AS country_code, ";
417         $sSQL .= "    get_address_by_language(place_id, -1, $sLanguagePrefArraySQL) AS langaddress,";
418         $sSQL .= "    get_name_by_language(name, $sLanguagePrefArraySQL) AS placename,";
419         $sSQL .= "    get_name_by_language(name, ARRAY['ref']) AS ref,";
420         if ($this->bIncludeExtraTags) $sSQL .= "hstore_to_json(extratags)::text AS extra,";
421         if ($this->bIncludeNameDetails) $sSQL .= "hstore_to_json(name)::text AS names,";
422         $sSQL .= "    avg(ST_X(centroid)) AS lon, ";
423         $sSQL .= "    avg(ST_Y(centroid)) AS lat, ";
424         $sSQL .= "    ".$sImportanceSQL."COALESCE(importance,0.75-(rank_search::float/40)) AS importance, ";
425         $sSQL .= "    ( ";
426         $sSQL .= "       SELECT max(p.importance*(p.rank_address+2))";
427         $sSQL .= "       FROM ";
428         $sSQL .= "         place_addressline s, ";
429         $sSQL .= "         placex p";
430         $sSQL .= "       WHERE s.place_id = min(CASE WHEN placex.rank_search < 28 THEN placex.place_id ELSE placex.parent_place_id END)";
431         $sSQL .= "         AND p.place_id = s.address_place_id ";
432         $sSQL .= "         AND s.isaddress ";
433         $sSQL .= "         AND p.importance is not null ";
434         $sSQL .= "    ) AS addressimportance, ";
435         $sSQL .= "    (extratags->'place') AS extra_place ";
436         $sSQL .= " FROM placex";
437         $sSQL .= " WHERE place_id in ($sPlaceIDs) ";
438         $sSQL .= "   AND (";
439         $sSQL .= "            placex.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank ";
440         if (14 >= $this->iMinAddressRank && 14 <= $this->iMaxAddressRank) {
441             $sSQL .= "        OR (extratags->'place') = 'city'";
442         }
443         if ($this->aAddressRankList) {
444             $sSQL .= "        OR placex.rank_address in (".join(',', $this->aAddressRankList).")";
445         }
446         $sSQL .= "       ) ";
447         if ($this->sAllowedTypesSQLList) {
448             $sSQL .= "AND placex.class in $this->sAllowedTypesSQLList ";
449         }
450         $sSQL .= "    AND linked_place_id is null ";
451         $sSQL .= " GROUP BY ";
452         $sSQL .= "     osm_type, ";
453         $sSQL .= "     osm_id, ";
454         $sSQL .= "     class, ";
455         $sSQL .= "     type, ";
456         $sSQL .= "     admin_level, ";
457         $sSQL .= "     rank_search, ";
458         $sSQL .= "     rank_address, ";
459         $sSQL .= "     calculated_country_code, ";
460         $sSQL .= "     importance, ";
461         if (!$this->bDeDupe) $sSQL .= "place_id,";
462         $sSQL .= "     langaddress, ";
463         $sSQL .= "     placename, ";
464         $sSQL .= "     ref, ";
465         if ($this->bIncludeExtraTags) $sSQL .= "extratags, ";
466         if ($this->bIncludeNameDetails) $sSQL .= "name, ";
467         $sSQL .= "     extratags->'place' ";
468
469         if (30 >= $this->iMinAddressRank && 30 <= $this->iMaxAddressRank) {
470             // only Tiger housenumbers and interpolation lines need to be interpolated, because they are saved as lines
471             // with start- and endnumber, the common osm housenumbers are usually saved as points
472             $sHousenumbers = "";
473             $i = 0;
474             $length = count($aPlaceIDs);
475             foreach ($aPlaceIDs as $placeID => $housenumber) {
476                 $i++;
477                 $sHousenumbers .= "(".$placeID.", ".$housenumber.")";
478                 if ($i<$length) $sHousenumbers .= ", ";
479             }
480             if (CONST_Use_US_Tiger_Data) {
481                 // Tiger search only if a housenumber was searched and if it was found (i.e. aPlaceIDs[placeID] = housenumber != -1) (realized through a join)
482                 $sSQL .= " union";
483                 $sSQL .= " SELECT ";
484                 $sSQL .= "     'T' AS osm_type, ";
485                 $sSQL .= "     place_id AS osm_id, ";
486                 $sSQL .= "     'place' AS class, ";
487                 $sSQL .= "     'house' AS type, ";
488                 $sSQL .= "     null AS admin_level, ";
489                 $sSQL .= "     30 AS rank_search, ";
490                 $sSQL .= "     30 AS rank_address, ";
491                 $sSQL .= "     min(place_id) AS place_id, ";
492                 $sSQL .= "     min(parent_place_id) AS parent_place_id, ";
493                 $sSQL .= "     'us' AS country_code, ";
494                 $sSQL .= "     get_address_by_language(place_id, housenumber_for_place, $sLanguagePrefArraySQL) AS langaddress,";
495                 $sSQL .= "     null AS placename, ";
496                 $sSQL .= "     null AS ref, ";
497                 if ($this->bIncludeExtraTags) $sSQL .= "null AS extra,";
498                 if ($this->bIncludeNameDetails) $sSQL .= "null AS names,";
499                 $sSQL .= "     avg(st_x(centroid)) AS lon, ";
500                 $sSQL .= "     avg(st_y(centroid)) AS lat,";
501                 $sSQL .= "     ".$sImportanceSQL."-1.15 AS importance, ";
502                 $sSQL .= "     (";
503                 $sSQL .= "        SELECT max(p.importance*(p.rank_address+2))";
504                 $sSQL .= "        FROM ";
505                 $sSQL .= "          place_addressline s, ";
506                 $sSQL .= "          placex p";
507                 $sSQL .= "        WHERE s.place_id = min(blub.parent_place_id)";
508                 $sSQL .= "          AND p.place_id = s.address_place_id ";
509                 $sSQL .= "          AND s.isaddress";
510                 $sSQL .= "          AND p.importance is not null";
511                 $sSQL .= "     ) AS addressimportance, ";
512                 $sSQL .= "     null AS extra_place ";
513                 $sSQL .= " FROM (";
514                 $sSQL .= "     SELECT place_id, ";    // interpolate the Tiger housenumbers here
515                 $sSQL .= "         ST_LineInterpolatePoint(linegeo, (housenumber_for_place-startnumber::float)/(endnumber-startnumber)::float) AS centroid, ";
516                 $sSQL .= "         parent_place_id, ";
517                 $sSQL .= "         housenumber_for_place";
518                 $sSQL .= "     FROM (";
519                 $sSQL .= "            location_property_tiger ";
520                 $sSQL .= "            JOIN (values ".$sHousenumbers.") AS housenumbers(place_id, housenumber_for_place) USING(place_id)) ";
521                 $sSQL .= "     WHERE ";
522                 $sSQL .= "         housenumber_for_place>=0";
523                 $sSQL .= "         AND 30 between $this->iMinAddressRank AND $this->iMaxAddressRank";
524                 $sSQL .= " ) AS blub"; //postgres wants an alias here
525                 $sSQL .= " GROUP BY";
526                 $sSQL .= "      place_id, ";
527                 $sSQL .= "      housenumber_for_place"; //is this group by really needed?, place_id + housenumber (in combination) are unique
528                 if (!$this->bDeDupe) $sSQL .= ", place_id ";
529             }
530             // osmline
531             // interpolation line search only if a housenumber was searched and if it was found (i.e. aPlaceIDs[placeID] = housenumber != -1) (realized through a join)
532             $sSQL .= " UNION ";
533             $sSQL .= "SELECT ";
534             $sSQL .= "  'W' AS osm_type, ";
535             $sSQL .= "  osm_id, ";
536             $sSQL .= "  'place' AS class, ";
537             $sSQL .= "  'house' AS type, ";
538             $sSQL .= "  null AS admin_level, ";
539             $sSQL .= "  30 AS rank_search, ";
540             $sSQL .= "  30 AS rank_address, ";
541             $sSQL .= "  min(place_id) as place_id, ";
542             $sSQL .= "  min(parent_place_id) AS parent_place_id, ";
543             $sSQL .= "  calculated_country_code AS country_code, ";
544             $sSQL .= "  get_address_by_language(place_id, housenumber_for_place, $sLanguagePrefArraySQL) AS langaddress, ";
545             $sSQL .= "  null AS placename, ";
546             $sSQL .= "  null AS ref, ";
547             if ($this->bIncludeExtraTags) $sSQL .= "null AS extra, ";
548             if ($this->bIncludeNameDetails) $sSQL .= "null AS names, ";
549             $sSQL .= "  AVG(st_x(centroid)) AS lon, ";
550             $sSQL .= "  AVG(st_y(centroid)) AS lat, ";
551             $sSQL .= "  ".$sImportanceSQL."-0.1 AS importance, ";  // slightly smaller than the importance for normal houses with rank 30, which is 0
552             $sSQL .= "  (";
553             $sSQL .= "     SELECT ";
554             $sSQL .= "       MAX(p.importance*(p.rank_address+2)) ";
555             $sSQL .= "     FROM";
556             $sSQL .= "       place_addressline s, ";
557             $sSQL .= "       placex p";
558             $sSQL .= "     WHERE s.place_id = min(blub.parent_place_id) ";
559             $sSQL .= "       AND p.place_id = s.address_place_id ";
560             $sSQL .= "       AND s.isaddress ";
561             $sSQL .= "       AND p.importance is not null";
562             $sSQL .= "  ) AS addressimportance,";
563             $sSQL .= "  null AS extra_place ";
564             $sSQL .= "  FROM (";
565             $sSQL .= "     SELECT ";
566             $sSQL .= "         osm_id, ";
567             $sSQL .= "         place_id, ";
568             $sSQL .= "         calculated_country_code, ";
569             $sSQL .= "         CASE ";             // interpolate the housenumbers here
570             $sSQL .= "           WHEN startnumber != endnumber ";
571             $sSQL .= "           THEN ST_LineInterpolatePoint(linegeo, (housenumber_for_place-startnumber::float)/(endnumber-startnumber)::float) ";
572             $sSQL .= "           ELSE ST_LineInterpolatePoint(linegeo, 0.5) ";
573             $sSQL .= "         END as centroid, ";
574             $sSQL .= "         parent_place_id, ";
575             $sSQL .= "         housenumber_for_place ";
576             $sSQL .= "     FROM (";
577             $sSQL .= "            location_property_osmline ";
578             $sSQL .= "            JOIN (values ".$sHousenumbers.") AS housenumbers(place_id, housenumber_for_place) USING(place_id)";
579             $sSQL .= "          ) ";
580             $sSQL .= "     WHERE housenumber_for_place>=0 ";
581             $sSQL .= "       AND 30 between $this->iMinAddressRank AND $this->iMaxAddressRank";
582             $sSQL .= "  ) as blub"; //postgres wants an alias here
583             $sSQL .= "  GROUP BY ";
584             $sSQL .= "    osm_id, ";
585             $sSQL .= "    place_id, ";
586             $sSQL .= "    housenumber_for_place, ";
587             $sSQL .= "    calculated_country_code "; //is this group by really needed?, place_id + housenumber (in combination) are unique
588             if (!$this->bDeDupe) $sSQL .= ", place_id ";
589
590             if (CONST_Use_Aux_Location_data) {
591                 $sSQL .= " UNION ";
592                 $sSQL .= "  SELECT ";
593                 $sSQL .= "     'L' AS osm_type, ";
594                 $sSQL .= "     place_id AS osm_id, ";
595                 $sSQL .= "     'place' AS class,";
596                 $sSQL .= "     'house' AS type, ";
597                 $sSQL .= "     null AS admin_level, ";
598                 $sSQL .= "     0 AS rank_search,";
599                 $sSQL .= "     0 AS rank_address, ";
600                 $sSQL .= "     min(place_id) AS place_id,";
601                 $sSQL .= "     min(parent_place_id) AS parent_place_id, ";
602                 $sSQL .= "     'us' AS country_code, ";
603                 $sSQL .= "     get_address_by_language(place_id, -1, $sLanguagePrefArraySQL) AS langaddress, ";
604                 $sSQL .= "     null AS placename, ";
605                 $sSQL .= "     null AS ref, ";
606                 if ($this->bIncludeExtraTags) $sSQL .= "null AS extra, ";
607                 if ($this->bIncludeNameDetails) $sSQL .= "null AS names, ";
608                 $sSQL .= "     avg(ST_X(centroid)) AS lon, ";
609                 $sSQL .= "     avg(ST_Y(centroid)) AS lat, ";
610                 $sSQL .= "     ".$sImportanceSQL."-1.10 AS importance, ";
611                 $sSQL .= "     ( ";
612                 $sSQL .= "       SELECT max(p.importance*(p.rank_address+2))";
613                 $sSQL .= "       FROM ";
614                 $sSQL .= "          place_addressline s, ";
615                 $sSQL .= "          placex p";
616                 $sSQL .= "       WHERE s.place_id = min(location_property_aux.parent_place_id)";
617                 $sSQL .= "         AND p.place_id = s.address_place_id ";
618                 $sSQL .= "         AND s.isaddress";
619                 $sSQL .= "         AND p.importance is not null";
620                 $sSQL .= "     ) AS addressimportance, ";
621                 $sSQL .= "     null AS extra_place ";
622                 $sSQL .= "  FROM location_property_aux ";
623                 $sSQL .= "  WHERE place_id in ($sPlaceIDs) ";
624                 $sSQL .= "    AND 30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
625                 $sSQL .= "  GROUP BY ";
626                 $sSQL .= "     place_id, ";
627                 if (!$this->bDeDupe) $sSQL .= "place_id, ";
628                 $sSQL .= "     get_address_by_language(place_id, -1, $sLanguagePrefArraySQL) ";
629             }
630         }
631
632         $sSQL .= " order by importance desc";
633         if (CONST_Debug) {
634             echo "<hr>";
635             var_dump($sSQL);
636         }
637         $aSearchResults = chksql(
638             $this->oDB->getAll($sSQL),
639             "Could not get details for place."
640         );
641
642         return $aSearchResults;
643     }
644
645     public function getGroupedSearches($aSearches, $aPhraseTypes, $aPhrases, $aValidTokens, $aWordFrequencyScores, $bStructuredPhrases)
646     {
647         /*
648              Calculate all searches using aValidTokens i.e.
649              'Wodsworth Road, Sheffield' =>
650
651              Phrase Wordset
652              0      0       (wodsworth road)
653              0      1       (wodsworth)(road)
654              1      0       (sheffield)
655
656              Score how good the search is so they can be ordered
657          */
658         foreach ($aPhrases as $iPhrase => $sPhrase) {
659             $aNewPhraseSearches = array();
660             if ($bStructuredPhrases) $sPhraseType = $aPhraseTypes[$iPhrase];
661             else $sPhraseType = '';
662
663             foreach ($aPhrases[$iPhrase]['wordsets'] as $iWordSet => $aWordset) {
664                 // Too many permutations - too expensive
665                 if ($iWordSet > 120) break;
666
667                 $aWordsetSearches = $aSearches;
668
669                 // Add all words from this wordset
670                 foreach ($aWordset as $iToken => $sToken) {
671                     //echo "<br><b>$sToken</b>";
672                     $aNewWordsetSearches = array();
673
674                     foreach ($aWordsetSearches as $aCurrentSearch) {
675                         //echo "<i>";
676                         //var_dump($aCurrentSearch);
677                         //echo "</i>";
678
679                         // If the token is valid
680                         if (isset($aValidTokens[' '.$sToken])) {
681                             foreach ($aValidTokens[' '.$sToken] as $aSearchTerm) {
682                                 $aSearch = $aCurrentSearch;
683                                 $aSearch['iSearchRank']++;
684                                 if (($sPhraseType == '' || $sPhraseType == 'country') && !empty($aSearchTerm['country_code']) && $aSearchTerm['country_code'] != '0') {
685                                     if ($aSearch['sCountryCode'] === false) {
686                                         $aSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
687                                         // Country is almost always at the end of the string - increase score for finding it anywhere else (optimisation)
688                                         if (($iToken+1 != sizeof($aWordset) || $iPhrase+1 != sizeof($aPhrases))) {
689                                             $aSearch['iSearchRank'] += 5;
690                                         }
691                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
692                                     }
693                                 } elseif (isset($aSearchTerm['lat']) && $aSearchTerm['lat'] !== '' && $aSearchTerm['lat'] !== null) {
694                                     if ($aSearch['fLat'] === '') {
695                                         $aSearch['fLat'] = $aSearchTerm['lat'];
696                                         $aSearch['fLon'] = $aSearchTerm['lon'];
697                                         $aSearch['fRadius'] = $aSearchTerm['radius'];
698                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
699                                     }
700                                 } elseif ($sPhraseType == 'postalcode') {
701                                     // We need to try the case where the postal code is the primary element (i.e. no way to tell if it is (postalcode, city) OR (city, postalcode) so try both
702                                     if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id']) {
703                                         // If we already have a name try putting the postcode first
704                                         if (sizeof($aSearch['aName'])) {
705                                             $aNewSearch = $aSearch;
706                                             $aNewSearch['aAddress'] = array_merge($aNewSearch['aAddress'], $aNewSearch['aName']);
707                                             $aNewSearch['aName'] = array();
708                                             $aNewSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
709                                             if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aNewSearch;
710                                         }
711
712                                         if (sizeof($aSearch['aName'])) {
713                                             if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strpos($sToken, ' ') !== false)) {
714                                                 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
715                                             } else {
716                                                 $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
717                                                 $aSearch['iSearchRank'] += 1000; // skip;
718                                             }
719                                         } else {
720                                             $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
721                                             //$aSearch['iNamePhrase'] = $iPhrase;
722                                         }
723                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
724                                     }
725                                 } elseif (($sPhraseType == '' || $sPhraseType == 'street') && $aSearchTerm['class'] == 'place' && $aSearchTerm['type'] == 'house') {
726                                     if ($aSearch['sHouseNumber'] === '') {
727                                         $aSearch['sHouseNumber'] = $sToken;
728                                         // sanity check: if the housenumber is not mainly made
729                                         // up of numbers, add a penalty
730                                         if (preg_match_all("/[^0-9]/", $sToken, $aMatches) > 2) $aSearch['iSearchRank']++;
731                                         // also housenumbers should appear in the first or second phrase
732                                         if ($iPhrase > 1) $aSearch['iSearchRank'] += 1;
733                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
734                                         /*
735                                         // Fall back to not searching for this item (better than nothing)
736                                         $aSearch = $aCurrentSearch;
737                                         $aSearch['iSearchRank'] += 1;
738                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
739                                          */
740                                     }
741                                 } elseif ($sPhraseType == '' && $aSearchTerm['class'] !== '' && $aSearchTerm['class'] !== null) {
742                                     if ($aSearch['sClass'] === '') {
743                                         $aSearch['sOperator'] = $aSearchTerm['operator'];
744                                         $aSearch['sClass'] = $aSearchTerm['class'];
745                                         $aSearch['sType'] = $aSearchTerm['type'];
746                                         if (sizeof($aSearch['aName'])) $aSearch['sOperator'] = 'name';
747                                         else $aSearch['sOperator'] = 'near'; // near = in for the moment
748                                         if (strlen($aSearchTerm['operator']) == 0) $aSearch['iSearchRank'] += 1;
749
750                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
751                                     }
752                                 } elseif (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id']) {
753                                     if (sizeof($aSearch['aName'])) {
754                                         if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strpos($sToken, ' ') !== false)) {
755                                             $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
756                                         } else {
757                                             $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
758                                             $aSearch['iSearchRank'] += 1000; // skip;
759                                         }
760                                     } else {
761                                         $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
762                                         //$aSearch['iNamePhrase'] = $iPhrase;
763                                     }
764                                     if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
765                                 }
766                             }
767                         }
768                         // Look for partial matches.
769                         // Note that there is no point in adding country terms here
770                         // because country are omitted in the address.
771                         if (isset($aValidTokens[$sToken]) && $sPhraseType != 'country') {
772                             // Allow searching for a word - but at extra cost
773                             foreach ($aValidTokens[$sToken] as $aSearchTerm) {
774                                 if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id']) {
775                                     if ((!$bStructuredPhrases || $iPhrase > 0) && sizeof($aCurrentSearch['aName']) && strpos($sToken, ' ') === false) {
776                                         $aSearch = $aCurrentSearch;
777                                         $aSearch['iSearchRank'] += 1;
778                                         if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency) {
779                                             $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
780                                             if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
781                                         } elseif (isset($aValidTokens[' '.$sToken])) { // revert to the token version?
782                                             $aSearch['aAddressNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
783                                             $aSearch['iSearchRank'] += 1;
784                                             if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
785                                             foreach ($aValidTokens[' '.$sToken] as $aSearchTermToken) {
786                                                 if (empty($aSearchTermToken['country_code'])
787                                                     && empty($aSearchTermToken['lat'])
788                                                     && empty($aSearchTermToken['class'])
789                                                 ) {
790                                                     $aSearch = $aCurrentSearch;
791                                                     $aSearch['iSearchRank'] += 1;
792                                                     $aSearch['aAddress'][$aSearchTermToken['word_id']] = $aSearchTermToken['word_id'];
793                                                     if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
794                                                 }
795                                             }
796                                         } else {
797                                             $aSearch['aAddressNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
798                                             if (preg_match('#^[0-9]+$#', $sToken)) $aSearch['iSearchRank'] += 2;
799                                             if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
800                                         }
801                                     }
802
803                                     if (!sizeof($aCurrentSearch['aName']) || $aCurrentSearch['iNamePhrase'] == $iPhrase) {
804                                         $aSearch = $aCurrentSearch;
805                                         $aSearch['iSearchRank'] += 1;
806                                         if (!sizeof($aCurrentSearch['aName'])) $aSearch['iSearchRank'] += 1;
807                                         if (preg_match('#^[0-9]+$#', $sToken)) $aSearch['iSearchRank'] += 2;
808                                         if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency) {
809                                             $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
810                                         } else {
811                                             $aSearch['aNameNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
812                                         }
813                                         $aSearch['iNamePhrase'] = $iPhrase;
814                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
815                                     }
816                                 }
817                             }
818                         } else {
819                             // Allow skipping a word - but at EXTREAM cost
820                             //$aSearch = $aCurrentSearch;
821                             //$aSearch['iSearchRank']+=100;
822                             //$aNewWordsetSearches[] = $aSearch;
823                         }
824                     }
825                     // Sort and cut
826                     usort($aNewWordsetSearches, 'bySearchRank');
827                     $aWordsetSearches = array_slice($aNewWordsetSearches, 0, 50);
828                 }
829                 //var_Dump('<hr>',sizeof($aWordsetSearches)); exit;
830
831                 $aNewPhraseSearches = array_merge($aNewPhraseSearches, $aNewWordsetSearches);
832                 usort($aNewPhraseSearches, 'bySearchRank');
833
834                 $aSearchHash = array();
835                 foreach ($aNewPhraseSearches as $iSearch => $aSearch) {
836                     $sHash = serialize($aSearch);
837                     if (isset($aSearchHash[$sHash])) unset($aNewPhraseSearches[$iSearch]);
838                     else $aSearchHash[$sHash] = 1;
839                 }
840
841                 $aNewPhraseSearches = array_slice($aNewPhraseSearches, 0, 50);
842             }
843
844             // Re-group the searches by their score, junk anything over 20 as just not worth trying
845             $aGroupedSearches = array();
846             foreach ($aNewPhraseSearches as $aSearch) {
847                 if ($aSearch['iSearchRank'] < $this->iMaxRank) {
848                     if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
849                     $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
850                 }
851             }
852             ksort($aGroupedSearches);
853
854             $iSearchCount = 0;
855             $aSearches = array();
856             foreach ($aGroupedSearches as $iScore => $aNewSearches) {
857                 $iSearchCount += sizeof($aNewSearches);
858                 $aSearches = array_merge($aSearches, $aNewSearches);
859                 if ($iSearchCount > 50) break;
860             }
861
862             //if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
863         }
864         return $aGroupedSearches;
865     }
866
867     /* Perform the actual query lookup.
868
869         Returns an ordered list of results, each with the following fields:
870             osm_type: type of corresponding OSM object
871                         N - node
872                         W - way
873                         R - relation
874                         P - postcode (internally computed)
875             osm_id: id of corresponding OSM object
876             class: general object class (corresponds to tag key of primary OSM tag)
877             type: subclass of object (corresponds to tag value of primary OSM tag)
878             admin_level: see http://wiki.openstreetmap.org/wiki/Admin_level
879             rank_search: rank in search hierarchy
880                         (see also http://wiki.openstreetmap.org/wiki/Nominatim/Development_overview#Country_to_street_level)
881             rank_address: rank in address hierarchy (determines orer in address)
882             place_id: internal key (may differ between different instances)
883             country_code: ISO country code
884             langaddress: localized full address
885             placename: localized name of object
886             ref: content of ref tag (if available)
887             lon: longitude
888             lat: latitude
889             importance: importance of place based on Wikipedia link count
890             addressimportance: cumulated importance of address elements
891             extra_place: type of place (for admin boundaries, if there is a place tag)
892             aBoundingBox: bounding Box
893             label: short description of the object class/type (English only)
894             name: full name (currently the same as langaddress)
895             foundorder: secondary ordering for places with same importance
896     */
897
898
899     public function lookup()
900     {
901         if (!$this->sQuery && !$this->aStructuredQuery) return false;
902
903         $sLanguagePrefArraySQL = "ARRAY[".join(',', array_map("getDBQuoted", $this->aLangPrefOrder))."]";
904         $sCountryCodesSQL = false;
905         if ($this->aCountryCodes) {
906             $sCountryCodesSQL = join(',', array_map('addQuotes', $this->aCountryCodes));
907         }
908
909         $sQuery = $this->sQuery;
910
911         // Conflicts between US state abreviations and various words for 'the' in different languages
912         if (isset($this->aLangPrefOrder['name:en'])) {
913             $sQuery = preg_replace('/(^|,)\s*il\s*(,|$)/', '\1illinois\2', $sQuery);
914             $sQuery = preg_replace('/(^|,)\s*al\s*(,|$)/', '\1alabama\2', $sQuery);
915             $sQuery = preg_replace('/(^|,)\s*la\s*(,|$)/', '\1louisiana\2', $sQuery);
916         }
917
918         $bBoundingBoxSearch = $this->bBoundedSearch && $this->sViewboxSmallSQL;
919         if ($this->sViewboxCentreSQL) {
920             // For complex viewboxes (routes) precompute the bounding geometry
921             $sGeom = chksql(
922                 $this->oDB->getOne("select ".$this->sViewboxSmallSQL),
923                 "Could not get small viewbox"
924             );
925             $this->sViewboxSmallSQL = "'".$sGeom."'::geometry";
926
927             $sGeom = chksql(
928                 $this->oDB->getOne("select ".$this->sViewboxLargeSQL),
929                 "Could not get large viewbox"
930             );
931             $this->sViewboxLargeSQL = "'".$sGeom."'::geometry";
932         }
933
934         // Do we have anything that looks like a lat/lon pair?
935         if ($aLooksLike = looksLikeLatLonPair($sQuery)) {
936             $this->setNearPoint(array($aLooksLike['lat'], $aLooksLike['lon']));
937             $sQuery = $aLooksLike['query'];
938         }
939
940         $aSearchResults = array();
941         if ($sQuery || $this->aStructuredQuery) {
942             // Start with a blank search
943             $aSearches = array(
944                           array(
945                            'iSearchRank' => 0,
946                            'iNamePhrase' => -1,
947                            'sCountryCode' => false,
948                            'aName' => array(),
949                            'aAddress' => array(),
950                            'aFullNameAddress' => array(),
951                            'aNameNonSearch' => array(),
952                            'aAddressNonSearch' => array(),
953                            'sOperator' => '',
954                            'aFeatureName' => array(),
955                            'sClass' => '',
956                            'sType' => '',
957                            'sHouseNumber' => '',
958                            'fLat' => '',
959                            'fLon' => '',
960                            'fRadius' => ''
961                           )
962                          );
963
964             // Do we have a radius search?
965             $sNearPointSQL = false;
966             if ($this->aNearPoint) {
967                 $sNearPointSQL = "ST_SetSRID(ST_Point(".(float)$this->aNearPoint[1].",".(float)$this->aNearPoint[0]."),4326)";
968                 $aSearches[0]['fLat'] = (float)$this->aNearPoint[0];
969                 $aSearches[0]['fLon'] = (float)$this->aNearPoint[1];
970                 $aSearches[0]['fRadius'] = (float)$this->aNearPoint[2];
971             }
972
973             // Any 'special' terms in the search?
974             $bSpecialTerms = false;
975             preg_match_all('/\\[(.*)=(.*)\\]/', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
976             $aSpecialTerms = array();
977             foreach ($aSpecialTermsRaw as $aSpecialTerm) {
978                 $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
979                 $aSpecialTerms[strtolower($aSpecialTerm[1])] = $aSpecialTerm[2];
980             }
981
982             preg_match_all('/\\[([\\w ]*)\\]/u', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
983             $aSpecialTerms = array();
984             if (isset($this->aStructuredQuery['amenity']) && $this->aStructuredQuery['amenity']) {
985                 $aSpecialTermsRaw[] = array('['.$this->aStructuredQuery['amenity'].']', $this->aStructuredQuery['amenity']);
986                 unset($this->aStructuredQuery['amenity']);
987             }
988
989             foreach ($aSpecialTermsRaw as $aSpecialTerm) {
990                 $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
991                 $sToken = chksql($this->oDB->getOne("SELECT make_standard_name('".$aSpecialTerm[1]."') AS string"));
992                 $sSQL = 'SELECT * ';
993                 $sSQL .= 'FROM ( ';
994                 $sSQL .= '   SELECT word_id, word_token, word, class, type, country_code, operator';
995                 $sSQL .= '   FROM word ';
996                 $sSQL .= '   WHERE word_token in (\' '.$sToken.'\')';
997                 $sSQL .= ') AS x ';
998                 $sSQL .= ' WHERE (class is not null AND class not in (\'place\')) ';
999                 $sSQL .= ' OR country_code is not null';
1000                 if (CONST_Debug) var_Dump($sSQL);
1001                 $aSearchWords = chksql($this->oDB->getAll($sSQL));
1002                 $aNewSearches = array();
1003                 foreach ($aSearches as $aSearch) {
1004                     foreach ($aSearchWords as $aSearchTerm) {
1005                         $aNewSearch = $aSearch;
1006                         if ($aSearchTerm['country_code']) {
1007                             $aNewSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
1008                             $aNewSearches[] = $aNewSearch;
1009                             $bSpecialTerms = true;
1010                         }
1011                         if ($aSearchTerm['class']) {
1012                             $aNewSearch['sClass'] = $aSearchTerm['class'];
1013                             $aNewSearch['sType'] = $aSearchTerm['type'];
1014                             $aNewSearches[] = $aNewSearch;
1015                             $bSpecialTerms = true;
1016                         }
1017                     }
1018                 }
1019                 $aSearches = $aNewSearches;
1020             }
1021
1022             // Split query into phrases
1023             // Commas are used to reduce the search space by indicating where phrases split
1024             if ($this->aStructuredQuery) {
1025                 $aPhrases = $this->aStructuredQuery;
1026                 $bStructuredPhrases = true;
1027             } else {
1028                 $aPhrases = explode(',', $sQuery);
1029                 $bStructuredPhrases = false;
1030             }
1031
1032             // Convert each phrase to standard form
1033             // Create a list of standard words
1034             // Get all 'sets' of words
1035             // Generate a complete list of all
1036             $aTokens = array();
1037             foreach ($aPhrases as $iPhrase => $sPhrase) {
1038                 $aPhrase = chksql(
1039                     $this->oDB->getRow("SELECT make_standard_name('".pg_escape_string($sPhrase)."') as string"),
1040                     "Cannot normalize query string (is it a UTF-8 string?)"
1041                 );
1042                 if (trim($aPhrase['string'])) {
1043                     $aPhrases[$iPhrase] = $aPhrase;
1044                     $aPhrases[$iPhrase]['words'] = explode(' ', $aPhrases[$iPhrase]['string']);
1045                     $aPhrases[$iPhrase]['wordsets'] = getWordSets($aPhrases[$iPhrase]['words'], 0);
1046                     $aTokens = array_merge($aTokens, getTokensFromSets($aPhrases[$iPhrase]['wordsets']));
1047                 } else {
1048                     unset($aPhrases[$iPhrase]);
1049                 }
1050             }
1051
1052             // Reindex phrases - we make assumptions later on that they are numerically keyed in order
1053             $aPhraseTypes = array_keys($aPhrases);
1054             $aPhrases = array_values($aPhrases);
1055
1056             if (sizeof($aTokens)) {
1057                 // Check which tokens we have, get the ID numbers
1058                 $sSQL = 'SELECT word_id, word_token, word, class, type, country_code, operator, search_name_count';
1059                 $sSQL .= ' FROM word ';
1060                 $sSQL .= ' WHERE word_token in ('.join(',', array_map("getDBQuoted", $aTokens)).')';
1061
1062                 if (CONST_Debug) var_Dump($sSQL);
1063
1064                 $aValidTokens = array();
1065                 if (sizeof($aTokens)) {
1066                     $aDatabaseWords = chksql(
1067                         $this->oDB->getAll($sSQL),
1068                         "Could not get word tokens."
1069                     );
1070                 } else {
1071                     $aDatabaseWords = array();
1072                 }
1073                 $aPossibleMainWordIDs = array();
1074                 $aWordFrequencyScores = array();
1075                 foreach ($aDatabaseWords as $aToken) {
1076                     // Very special case - require 2 letter country param to match the country code found
1077                     if ($bStructuredPhrases && $aToken['country_code'] && !empty($this->aStructuredQuery['country'])
1078                         && strlen($this->aStructuredQuery['country']) == 2 && strtolower($this->aStructuredQuery['country']) != $aToken['country_code']
1079                     ) {
1080                         continue;
1081                     }
1082
1083                     if (isset($aValidTokens[$aToken['word_token']])) {
1084                         $aValidTokens[$aToken['word_token']][] = $aToken;
1085                     } else {
1086                         $aValidTokens[$aToken['word_token']] = array($aToken);
1087                     }
1088                     if (!$aToken['class'] && !$aToken['country_code']) $aPossibleMainWordIDs[$aToken['word_id']] = 1;
1089                     $aWordFrequencyScores[$aToken['word_id']] = $aToken['search_name_count'] + 1;
1090                 }
1091                 if (CONST_Debug) var_Dump($aPhrases, $aValidTokens);
1092
1093                 // Try and calculate GB postcodes we might be missing
1094                 foreach ($aTokens as $sToken) {
1095                     // Source of gb postcodes is now definitive - always use
1096                     if (preg_match('/^([A-Z][A-Z]?[0-9][0-9A-Z]? ?[0-9])([A-Z][A-Z])$/', strtoupper(trim($sToken)), $aData)) {
1097                         if (substr($aData[1], -2, 1) != ' ') {
1098                             $aData[0] = substr($aData[0], 0, strlen($aData[1])-1).' '.substr($aData[0], strlen($aData[1])-1);
1099                             $aData[1] = substr($aData[1], 0, -1).' '.substr($aData[1], -1, 1);
1100                         }
1101                         $aGBPostcodeLocation = gbPostcodeCalculate($aData[0], $aData[1], $aData[2], $this->oDB);
1102                         if ($aGBPostcodeLocation) {
1103                             $aValidTokens[$sToken] = $aGBPostcodeLocation;
1104                         }
1105                     } elseif (!isset($aValidTokens[$sToken]) && preg_match('/^([0-9]{5}) [0-9]{4}$/', $sToken, $aData)) {
1106                         // US ZIP+4 codes - if there is no token,
1107                         // merge in the 5-digit ZIP code
1108                         if (isset($aValidTokens[$aData[1]])) {
1109                             foreach ($aValidTokens[$aData[1]] as $aToken) {
1110                                 if (!$aToken['class']) {
1111                                     if (isset($aValidTokens[$sToken])) {
1112                                         $aValidTokens[$sToken][] = $aToken;
1113                                     } else {
1114                                         $aValidTokens[$sToken] = array($aToken);
1115                                     }
1116                                 }
1117                             }
1118                         }
1119                     }
1120                 }
1121
1122                 foreach ($aTokens as $sToken) {
1123                     // Unknown single word token with a number - assume it is a house number
1124                     if (!isset($aValidTokens[' '.$sToken]) && strpos($sToken, ' ') === false && preg_match('/[0-9]/', $sToken)) {
1125                         $aValidTokens[' '.$sToken] = array(array('class' => 'place', 'type' => 'house'));
1126                     }
1127                 }
1128
1129                 // Any words that have failed completely?
1130                 // TODO: suggestions
1131
1132                 // Start the search process
1133                 // array with: placeid => -1 | tiger-housenumber
1134                 $aResultPlaceIDs = array();
1135
1136                 $aGroupedSearches = $this->getGroupedSearches($aSearches, $aPhraseTypes, $aPhrases, $aValidTokens, $aWordFrequencyScores, $bStructuredPhrases);
1137
1138                 if ($this->bReverseInPlan) {
1139                     // Reverse phrase array and also reverse the order of the wordsets in
1140                     // the first and final phrase. Don't bother about phrases in the middle
1141                     // because order in the address doesn't matter.
1142                     $aPhrases = array_reverse($aPhrases);
1143                     $aPhrases[0]['wordsets'] = getInverseWordSets($aPhrases[0]['words'], 0);
1144                     if (sizeof($aPhrases) > 1) {
1145                         $aFinalPhrase = end($aPhrases);
1146                         $aPhrases[sizeof($aPhrases)-1]['wordsets'] = getInverseWordSets($aFinalPhrase['words'], 0);
1147                     }
1148                     $aReverseGroupedSearches = $this->getGroupedSearches($aSearches, null, $aPhrases, $aValidTokens, $aWordFrequencyScores, false);
1149
1150                     foreach ($aGroupedSearches as $aSearches) {
1151                         foreach ($aSearches as $aSearch) {
1152                             if ($aSearch['iSearchRank'] < $this->iMaxRank) {
1153                                 if (!isset($aReverseGroupedSearches[$aSearch['iSearchRank']])) $aReverseGroupedSearches[$aSearch['iSearchRank']] = array();
1154                                 $aReverseGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
1155                             }
1156                         }
1157                     }
1158
1159                     $aGroupedSearches = $aReverseGroupedSearches;
1160                     ksort($aGroupedSearches);
1161                 }
1162             } else {
1163                 // Re-group the searches by their score, junk anything over 20 as just not worth trying
1164                 $aGroupedSearches = array();
1165                 foreach ($aSearches as $aSearch) {
1166                     if ($aSearch['iSearchRank'] < $this->iMaxRank) {
1167                         if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
1168                         $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
1169                     }
1170                 }
1171                 ksort($aGroupedSearches);
1172             }
1173
1174             if (CONST_Debug) var_Dump($aGroupedSearches);
1175             if (CONST_Search_TryDroppedAddressTerms && sizeof($this->aStructuredQuery) > 0) {
1176                 $aCopyGroupedSearches = $aGroupedSearches;
1177                 foreach ($aCopyGroupedSearches as $iGroup => $aSearches) {
1178                     foreach ($aSearches as $iSearch => $aSearch) {
1179                         $aReductionsList = array($aSearch['aAddress']);
1180                         $iSearchRank = $aSearch['iSearchRank'];
1181                         while (sizeof($aReductionsList) > 0) {
1182                             $iSearchRank += 5;
1183                             if ($iSearchRank > iMaxRank) break 3;
1184                             $aNewReductionsList = array();
1185                             foreach ($aReductionsList as $aReductionsWordList) {
1186                                 for ($iReductionWord = 0; $iReductionWord < sizeof($aReductionsWordList); $iReductionWord++) {
1187                                     $aReductionsWordListResult = array_merge(array_slice($aReductionsWordList, 0, $iReductionWord), array_slice($aReductionsWordList, $iReductionWord+1));
1188                                     $aReverseSearch = $aSearch;
1189                                     $aSearch['aAddress'] = $aReductionsWordListResult;
1190                                     $aSearch['iSearchRank'] = $iSearchRank;
1191                                     $aGroupedSearches[$iSearchRank][] = $aReverseSearch;
1192                                     if (sizeof($aReductionsWordListResult) > 0) {
1193                                         $aNewReductionsList[] = $aReductionsWordListResult;
1194                                     }
1195                                 }
1196                             }
1197                             $aReductionsList = $aNewReductionsList;
1198                         }
1199                     }
1200                 }
1201                 ksort($aGroupedSearches);
1202             }
1203
1204             // Filter out duplicate searches
1205             $aSearchHash = array();
1206             foreach ($aGroupedSearches as $iGroup => $aSearches) {
1207                 foreach ($aSearches as $iSearch => $aSearch) {
1208                     $sHash = serialize($aSearch);
1209                     if (isset($aSearchHash[$sHash])) {
1210                         unset($aGroupedSearches[$iGroup][$iSearch]);
1211                         if (sizeof($aGroupedSearches[$iGroup]) == 0) unset($aGroupedSearches[$iGroup]);
1212                     } else {
1213                         $aSearchHash[$sHash] = 1;
1214                     }
1215                 }
1216             }
1217
1218             if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
1219
1220             $iGroupLoop = 0;
1221             $iQueryLoop = 0;
1222             foreach ($aGroupedSearches as $iGroupedRank => $aSearches) {
1223                 $iGroupLoop++;
1224                 foreach ($aSearches as $aSearch) {
1225                     $iQueryLoop++;
1226                     $searchedHousenumber = -1;
1227
1228                     if (CONST_Debug) echo "<hr><b>Search Loop, group $iGroupLoop, loop $iQueryLoop</b>";
1229                     if (CONST_Debug) _debugDumpGroupedSearches(array($iGroupedRank => array($aSearch)), $aValidTokens);
1230
1231                     // No location term?
1232                     if (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && !$aSearch['fLon']) {
1233                         if ($aSearch['sCountryCode'] && !$aSearch['sClass'] && !$aSearch['sHouseNumber']) {
1234                             // Just looking for a country by code - look it up
1235                             if (4 >= $this->iMinAddressRank && 4 <= $this->iMaxAddressRank) {
1236                                 $sSQL = "SELECT place_id FROM placex WHERE calculated_country_code='".$aSearch['sCountryCode']."' AND rank_search = 4";
1237                                 if ($sCountryCodesSQL) $sSQL .= " AND calculated_country_code in ($sCountryCodesSQL)";
1238                                 if ($bBoundingBoxSearch)
1239                                     $sSQL .= " AND _st_intersects($this->sViewboxSmallSQL, geometry)";
1240                                 $sSQL .= " ORDER BY st_area(geometry) DESC LIMIT 1";
1241                                 if (CONST_Debug) var_dump($sSQL);
1242                                 $aPlaceIDs = chksql($this->oDB->getCol($sSQL));
1243                             } else {
1244                                 $aPlaceIDs = array();
1245                             }
1246                         } else {
1247                             if (!$bBoundingBoxSearch && !$aSearch['fLon']) continue;
1248                             if (!$aSearch['sClass']) continue;
1249
1250                             $sSQL = "SELECT COUNT(*) FROM pg_tables WHERE tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1251                             if (chksql($this->oDB->getOne($sSQL))) {
1252                                 $sSQL = "SELECT place_id FROM place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
1253                                 if ($sCountryCodesSQL) $sSQL .= " JOIN placex USING (place_id)";
1254                                 $sSQL .= " WHERE st_contains($this->sViewboxSmallSQL, ct.centroid)";
1255                                 if ($sCountryCodesSQL) $sSQL .= " AND calculated_country_code in ($sCountryCodesSQL)";
1256                                 if (sizeof($this->aExcludePlaceIDs)) {
1257                                     $sSQL .= " AND place_id not in (".join(',', $this->aExcludePlaceIDs).")";
1258                                 }
1259                                 if ($this->sViewboxCentreSQL) $sSQL .= " ORDER BY ST_Distance($this->sViewboxCentreSQL, ct.centroid) ASC";
1260                                 $sSQL .= " limit $this->iLimit";
1261                                 if (CONST_Debug) var_dump($sSQL);
1262                                 $aPlaceIDs = chksql($this->oDB->getCol($sSQL));
1263
1264                                 // If excluded place IDs are given, it is fair to assume that
1265                                 // there have been results in the small box, so no further
1266                                 // expansion in that case.
1267                                 // Also don't expand if bounded results were requested.
1268                                 if (!sizeof($aPlaceIDs) && !sizeof($this->aExcludePlaceIDs) && !$this->bBoundedSearch) {
1269                                     $sSQL = "SELECT place_id FROM place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
1270                                     if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
1271                                     $sSQL .= " WHERE ST_Contains($this->sViewboxLargeSQL, ct.centroid)";
1272                                     if ($sCountryCodesSQL) $sSQL .= " AND calculated_country_code in ($sCountryCodesSQL)";
1273                                     if ($this->sViewboxCentreSQL) $sSQL .= " ORDER BY ST_Distance($this->sViewboxCentreSQL, ct.centroid) ASC";
1274                                     $sSQL .= " LIMIT $this->iLimit";
1275                                     if (CONST_Debug) var_dump($sSQL);
1276                                     $aPlaceIDs = chksql($this->oDB->getCol($sSQL));
1277                                 }
1278                             } else {
1279                                 $sSQL = "SELECT place_id ";
1280                                 $sSQL .= "FROM placex ";
1281                                 $sSQL .= "WHERE class='".$aSearch['sClass']."' ";
1282                                 $sSQL .= "  AND type='".$aSearch['sType']."'";
1283                                 $sSQL .= "  AND ST_Contains($this->sViewboxSmallSQL, geometry) ";
1284                                 $sSQL .= "  AND linked_place_id is null";
1285                                 if ($sCountryCodesSQL) $sSQL .= " AND calculated_country_code in ($sCountryCodesSQL)";
1286                                 if ($this->sViewboxCentreSQL)   $sSQL .= " ORDER BY ST_Distance($this->sViewboxCentreSQL, centroid) ASC";
1287                                 $sSQL .= " LIMIT $this->iLimit";
1288                                 if (CONST_Debug) var_dump($sSQL);
1289                                 $aPlaceIDs = chksql($this->oDB->getCol($sSQL));
1290                             }
1291                         }
1292                     } elseif ($aSearch['fLon'] && !sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && !$aSearch['sClass']) {
1293                         // If a coordinate is given, the search must either
1294                         // be for a name or a special search. Ignore everythin else.
1295                         $aPlaceIDs = array();
1296                     } else {
1297                         $aPlaceIDs = array();
1298
1299                         // First we need a position, either aName or fLat or both
1300                         $aTerms = array();
1301                         $aOrder = array();
1302
1303                         if ($aSearch['sHouseNumber'] && sizeof($aSearch['aAddress'])) {
1304                             $sHouseNumberRegex = '\\\\m'.$aSearch['sHouseNumber'].'\\\\M';
1305                             $aOrder[] = "";
1306                             $aOrder[0] = "  (";
1307                             $aOrder[0] .= "   EXISTS(";
1308                             $aOrder[0] .= "     SELECT place_id ";
1309                             $aOrder[0] .= "     FROM placex ";
1310                             $aOrder[0] .= "     WHERE parent_place_id = search_name.place_id";
1311                             $aOrder[0] .= "       AND transliteration(housenumber) ~* E'".$sHouseNumberRegex."' ";
1312                             $aOrder[0] .= "     LIMIT 1";
1313                             $aOrder[0] .= "   ) ";
1314                             // also housenumbers from interpolation lines table are needed
1315                             $aOrder[0] .= "   OR EXISTS(";
1316                             $aOrder[0] .= "     SELECT place_id ";
1317                             $aOrder[0] .= "     FROM location_property_osmline ";
1318                             $aOrder[0] .= "     WHERE parent_place_id = search_name.place_id";
1319                             $aOrder[0] .= "       AND ".intval($aSearch['sHouseNumber']).">=startnumber ";
1320                             $aOrder[0] .= "       AND ".intval($aSearch['sHouseNumber'])."<=endnumber ";
1321                             $aOrder[0] .= "     LIMIT 1";
1322                             $aOrder[0] .= "   )";
1323                             $aOrder[0] .= " )";
1324                             $aOrder[0] .= " DESC";
1325                         }
1326
1327                         // TODO: filter out the pointless search terms (2 letter name tokens and less)
1328                         // they might be right - but they are just too darned expensive to run
1329                         if (sizeof($aSearch['aName'])) $aTerms[] = "name_vector @> ARRAY[".join($aSearch['aName'], ",")."]";
1330                         if (sizeof($aSearch['aNameNonSearch'])) $aTerms[] = "array_cat(name_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aNameNonSearch'], ",")."]";
1331                         if (sizeof($aSearch['aAddress']) && $aSearch['aName'] != $aSearch['aAddress']) {
1332                             // For infrequent name terms disable index usage for address
1333                             if (CONST_Search_NameOnlySearchFrequencyThreshold
1334                                 && sizeof($aSearch['aName']) == 1
1335                                 && $aWordFrequencyScores[$aSearch['aName'][reset($aSearch['aName'])]] < CONST_Search_NameOnlySearchFrequencyThreshold
1336                             ) {
1337                                 $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join(array_merge($aSearch['aAddress'], $aSearch['aAddressNonSearch']), ",")."]";
1338                             } else {
1339                                 $aTerms[] = "nameaddress_vector @> ARRAY[".join($aSearch['aAddress'], ",")."]";
1340                                 if (sizeof($aSearch['aAddressNonSearch'])) {
1341                                     $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aAddressNonSearch'], ",")."]";
1342                                 }
1343                             }
1344                         }
1345                         if ($aSearch['sCountryCode']) $aTerms[] = "country_code = '".pg_escape_string($aSearch['sCountryCode'])."'";
1346                         if ($aSearch['sHouseNumber']) {
1347                             $aTerms[] = "address_rank between 16 and 27";
1348                         } else {
1349                             if ($this->iMinAddressRank > 0) {
1350                                 $aTerms[] = "address_rank >= ".$this->iMinAddressRank;
1351                             }
1352                             if ($this->iMaxAddressRank < 30) {
1353                                 $aTerms[] = "address_rank <= ".$this->iMaxAddressRank;
1354                             }
1355                         }
1356                         if ($aSearch['fLon'] && $aSearch['fLat']) {
1357                             $aTerms[] = sprintf(
1358                                 'ST_DWithin(centroid, ST_SetSRID(ST_Point(%F,%F),4326), %F)',
1359                                 $aSearch['fLon'],
1360                                 $aSearch['fLat'],
1361                                 $aSearch['fRadius']
1362                             );
1363
1364                             $aOrder[] = "ST_Distance(centroid, ST_SetSRID(ST_Point(".$aSearch['fLon'].",".$aSearch['fLat']."),4326)) ASC";
1365                         }
1366                         if (sizeof($this->aExcludePlaceIDs)) {
1367                             $aTerms[] = "place_id not in (".join(',', $this->aExcludePlaceIDs).")";
1368                         }
1369                         if ($sCountryCodesSQL) {
1370                             $aTerms[] = "country_code in ($sCountryCodesSQL)";
1371                         }
1372
1373                         if ($bBoundingBoxSearch) $aTerms[] = "centroid && $this->sViewboxSmallSQL";
1374                         if ($sNearPointSQL) $aOrder[] = "ST_Distance($sNearPointSQL, centroid) ASC";
1375
1376                         if ($aSearch['sHouseNumber']) {
1377                             $sImportanceSQL = '- abs(26 - address_rank) + 3';
1378                         } else {
1379                             $sImportanceSQL = '(CASE WHEN importance = 0 OR importance IS NULL THEN 0.75-(search_rank::float/40) ELSE importance END)';
1380                         }
1381                         if ($this->sViewboxSmallSQL) $sImportanceSQL .= " * CASE WHEN ST_Contains($this->sViewboxSmallSQL, centroid) THEN 1 ELSE 0.5 END";
1382                         if ($this->sViewboxLargeSQL) $sImportanceSQL .= " * CASE WHEN ST_Contains($this->sViewboxLargeSQL, centroid) THEN 1 ELSE 0.5 END";
1383
1384                         $aOrder[] = "$sImportanceSQL DESC";
1385                         if (sizeof($aSearch['aFullNameAddress'])) {
1386                             $sExactMatchSQL = ' ( ';
1387                             $sExactMatchSQL .= '   SELECT count(*) FROM ( ';
1388                             $sExactMatchSQL .= '      SELECT unnest(ARRAY['.join($aSearch['aFullNameAddress'], ",").']) ';
1389                             $sExactMatchSQL .= '      INTERSECT ';
1390                             $sExactMatchSQL .= '      SELECT unnest(nameaddress_vector)';
1391                             $sExactMatchSQL .= '   ) s';
1392                             $sExactMatchSQL .= ') as exactmatch';
1393                             $aOrder[] = 'exactmatch DESC';
1394                         } else {
1395                             $sExactMatchSQL = '0::int as exactmatch';
1396                         }
1397
1398                         if (sizeof($aTerms)) {
1399                             $sSQL = "SELECT place_id, ";
1400                             $sSQL .= $sExactMatchSQL;
1401                             $sSQL .= " FROM search_name";
1402                             $sSQL .= " WHERE ".join(' and ', $aTerms);
1403                             $sSQL .= " ORDER BY ".join(', ', $aOrder);
1404                             if ($aSearch['sHouseNumber'] || $aSearch['sClass']) {
1405                                 $sSQL .= " LIMIT 20";
1406                             } elseif (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && $aSearch['sClass']) {
1407                                 $sSQL .= " LIMIT 1";
1408                             } else {
1409                                 $sSQL .= " LIMIT ".$this->iLimit;
1410                             }
1411
1412                             if (CONST_Debug) var_dump($sSQL);
1413                             $aViewBoxPlaceIDs = chksql(
1414                                 $this->oDB->getAll($sSQL),
1415                                 "Could not get places for search terms."
1416                             );
1417                             //var_dump($aViewBoxPlaceIDs);
1418                             // Did we have an viewbox matches?
1419                             $aPlaceIDs = array();
1420                             $bViewBoxMatch = false;
1421                             foreach ($aViewBoxPlaceIDs as $aViewBoxRow) {
1422                                 //if ($bViewBoxMatch == 1 && $aViewBoxRow['in_small'] == 'f') break;
1423                                 //if ($bViewBoxMatch == 2 && $aViewBoxRow['in_large'] == 'f') break;
1424                                 //if ($aViewBoxRow['in_small'] == 't') $bViewBoxMatch = 1;
1425                                 //else if ($aViewBoxRow['in_large'] == 't') $bViewBoxMatch = 2;
1426                                 $aPlaceIDs[] = $aViewBoxRow['place_id'];
1427                                 $this->exactMatchCache[$aViewBoxRow['place_id']] = $aViewBoxRow['exactmatch'];
1428                             }
1429                         }
1430                         //var_Dump($aPlaceIDs);
1431                         //exit;
1432
1433                         //now search for housenumber, if housenumber provided
1434                         if ($aSearch['sHouseNumber'] && sizeof($aPlaceIDs)) {
1435                             $searchedHousenumber = intval($aSearch['sHouseNumber']);
1436                             $aRoadPlaceIDs = $aPlaceIDs;
1437                             $sPlaceIDs = join(',', $aPlaceIDs);
1438
1439                             // Now they are indexed, look for a house attached to a street we found
1440                             $sHouseNumberRegex = '\\\\m'.$aSearch['sHouseNumber'].'\\\\M';
1441                             $sSQL = "SELECT place_id FROM placex ";
1442                             $sSQL .= "WHERE parent_place_id in (".$sPlaceIDs.") and transliteration(housenumber) ~* E'".$sHouseNumberRegex."'";
1443                             if (sizeof($this->aExcludePlaceIDs)) {
1444                                 $sSQL .= " AND place_id not in (".join(',', $this->aExcludePlaceIDs).")";
1445                             }
1446                             $sSQL .= " LIMIT $this->iLimit";
1447                             if (CONST_Debug) var_dump($sSQL);
1448                             $aPlaceIDs = chksql($this->oDB->getCol($sSQL));
1449
1450                             // if nothing found, search in the interpolation line table
1451                             if (!sizeof($aPlaceIDs)) {
1452                                 // do we need to use transliteration and the regex for housenumbers???
1453                                 //new query for lines, not housenumbers anymore
1454                                 $sSQL = "SELECT distinct place_id FROM location_property_osmline";
1455                                 $sSQL .= " WHERE parent_place_id in (".$sPlaceIDs.") and (";
1456                                 if ($searchedHousenumber%2 == 0) {
1457                                     //if housenumber is even, look for housenumber in streets with interpolationtype even or all
1458                                     $sSQL .= "interpolationtype='even'";
1459                                 } else {
1460                                     //look for housenumber in streets with interpolationtype odd or all
1461                                     $sSQL .= "interpolationtype='odd'";
1462                                 }
1463                                 $sSQL .= " or interpolationtype='all') and ";
1464                                 $sSQL .= $searchedHousenumber.">=startnumber and ";
1465                                 $sSQL .= $searchedHousenumber."<=endnumber";
1466
1467                                 if (sizeof($this->aExcludePlaceIDs)) {
1468                                     $sSQL .= " AND place_id not in (".join(',', $this->aExcludePlaceIDs).")";
1469                                 }
1470                                 //$sSQL .= " limit $this->iLimit";
1471                                 if (CONST_Debug) var_dump($sSQL);
1472                                 //get place IDs
1473                                 $aPlaceIDs = chksql($this->oDB->getCol($sSQL, 0));
1474                             }
1475
1476                             // If nothing found try the aux fallback table
1477                             if (CONST_Use_Aux_Location_data && !sizeof($aPlaceIDs)) {
1478                                 $sSQL = "SELECT place_id FROM location_property_aux ";
1479                                 $sSQL .= " WHERE parent_place_id in (".$sPlaceIDs.") ";
1480                                 $sSQL .= " AND housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1481                                 if (sizeof($this->aExcludePlaceIDs)) {
1482                                     $sSQL .= " AND parent_place_id not in (".join(',', $this->aExcludePlaceIDs).")";
1483                                 }
1484                                 //$sSQL .= " limit $this->iLimit";
1485                                 if (CONST_Debug) var_dump($sSQL);
1486                                 $aPlaceIDs = chksql($this->oDB->getCol($sSQL));
1487                             }
1488
1489                             //if nothing was found in placex or location_property_aux, then search in Tiger data for this housenumber(location_property_tiger)
1490                             if (CONST_Use_US_Tiger_Data && !sizeof($aPlaceIDs)) {
1491                                 $sSQL = "SELECT distinct place_id FROM location_property_tiger";
1492                                 $sSQL .= " WHERE parent_place_id in (".$sPlaceIDs.") and (";
1493                                 if ($searchedHousenumber%2 == 0) {
1494                                     $sSQL .= "interpolationtype='even'";
1495                                 } else {
1496                                     $sSQL .= "interpolationtype='odd'";
1497                                 }
1498                                 $sSQL .= " or interpolationtype='all') and ";
1499                                 $sSQL .= $searchedHousenumber.">=startnumber and ";
1500                                 $sSQL .= $searchedHousenumber."<=endnumber";
1501
1502                                 if (sizeof($this->aExcludePlaceIDs)) {
1503                                     $sSQL .= " AND place_id not in (".join(',', $this->aExcludePlaceIDs).")";
1504                                 }
1505                                 //$sSQL .= " limit $this->iLimit";
1506                                 if (CONST_Debug) var_dump($sSQL);
1507                                 //get place IDs
1508                                 $aPlaceIDs = chksql($this->oDB->getCol($sSQL, 0));
1509                             }
1510
1511                             // Fallback to the road (if no housenumber was found)
1512                             if (!sizeof($aPlaceIDs) && preg_match('/[0-9]+/', $aSearch['sHouseNumber'])) {
1513                                 $aPlaceIDs = $aRoadPlaceIDs;
1514                                 //set to -1, if no housenumbers were found
1515                                 $searchedHousenumber = -1;
1516                             }
1517                             //else: housenumber was found, remains saved in searchedHousenumber
1518                         }
1519
1520
1521                         if ($aSearch['sClass'] && sizeof($aPlaceIDs)) {
1522                             $sPlaceIDs = join(',', $aPlaceIDs);
1523                             $aClassPlaceIDs = array();
1524
1525                             if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'name') {
1526                                 // If they were searching for a named class (i.e. 'Kings Head pub') then we might have an extra match
1527                                 $sSQL = "SELECT place_id ";
1528                                 $sSQL .= " FROM placex ";
1529                                 $sSQL .= " WHERE place_id in ($sPlaceIDs) ";
1530                                 $sSQL .= "   AND class='".$aSearch['sClass']."' ";
1531                                 $sSQL .= "   AND type='".$aSearch['sType']."'";
1532                                 $sSQL .= "   AND linked_place_id is null";
1533                                 if ($sCountryCodesSQL) $sSQL .= " AND calculated_country_code in ($sCountryCodesSQL)";
1534                                 $sSQL .= " ORDER BY rank_search ASC ";
1535                                 $sSQL .= " LIMIT $this->iLimit";
1536                                 if (CONST_Debug) var_dump($sSQL);
1537                                 $aClassPlaceIDs = chksql($this->oDB->getCol($sSQL));
1538                             }
1539
1540                             if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'near') { // & in
1541                                 $sSQL = "SELECT count(*) FROM pg_tables ";
1542                                 $sSQL .= "WHERE tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1543                                 $bCacheTable = chksql($this->oDB->getOne($sSQL));
1544
1545                                 $sSQL = "SELECT min(rank_search) FROM placex WHERE place_id in ($sPlaceIDs)";
1546
1547                                 if (CONST_Debug) var_dump($sSQL);
1548                                 $this->iMaxRank = ((int)chksql($this->oDB->getOne($sSQL)));
1549
1550                                 // For state / country level searches the normal radius search doesn't work very well
1551                                 $sPlaceGeom = false;
1552                                 if ($this->iMaxRank < 9 && $bCacheTable) {
1553                                     // Try and get a polygon to search in instead
1554                                     $sSQL = "SELECT geometry ";
1555                                     $sSQL .= " FROM placex";
1556                                     $sSQL .= " WHERE place_id in ($sPlaceIDs)";
1557                                     $sSQL .= "   AND rank_search < $this->iMaxRank + 5";
1558                                     $sSQL .= "   AND ST_Geometrytype(geometry) in ('ST_Polygon','ST_MultiPolygon')";
1559                                     $sSQL .= " ORDER BY rank_search ASC ";
1560                                     $sSQL .= " LIMIT 1";
1561                                     if (CONST_Debug) var_dump($sSQL);
1562                                     $sPlaceGeom = chksql($this->oDB->getOne($sSQL));
1563                                 }
1564
1565                                 if ($sPlaceGeom) {
1566                                     $sPlaceIDs = false;
1567                                 } else {
1568                                     $this->iMaxRank += 5;
1569                                     $sSQL = "SELECT place_id FROM placex WHERE place_id in ($sPlaceIDs) and rank_search < $this->iMaxRank";
1570                                     if (CONST_Debug) var_dump($sSQL);
1571                                     $aPlaceIDs = chksql($this->oDB->getCol($sSQL));
1572                                     $sPlaceIDs = join(',', $aPlaceIDs);
1573                                 }
1574
1575                                 if ($sPlaceIDs || $sPlaceGeom) {
1576                                     $fRange = 0.01;
1577                                     if ($bCacheTable) {
1578                                         // More efficient - can make the range bigger
1579                                         $fRange = 0.05;
1580
1581                                         $sOrderBySQL = '';
1582                                         if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.centroid)";
1583                                         elseif ($sPlaceIDs) $sOrderBySQL = "ST_Distance(l.centroid, f.geometry)";
1584                                         elseif ($sPlaceGeom) $sOrderBysSQL = "ST_Distance(st_centroid('".$sPlaceGeom."'), l.centroid)";
1585
1586                                         $sSQL = "select distinct l.place_id".($sOrderBySQL?','.$sOrderBySQL:'')." from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." as l";
1587                                         if ($sCountryCodesSQL) $sSQL .= " join placex as lp using (place_id)";
1588                                         if ($sPlaceIDs) {
1589                                             $sSQL .= ",placex as f where ";
1590                                             $sSQL .= "f.place_id in ($sPlaceIDs) and ST_DWithin(l.centroid, f.centroid, $fRange) ";
1591                                         }
1592                                         if ($sPlaceGeom) {
1593                                             $sSQL .= " where ";
1594                                             $sSQL .= "ST_Contains('".$sPlaceGeom."', l.centroid) ";
1595                                         }
1596                                         if (sizeof($this->aExcludePlaceIDs)) {
1597                                             $sSQL .= " and l.place_id not in (".join(',', $this->aExcludePlaceIDs).")";
1598                                         }
1599                                         if ($sCountryCodesSQL) $sSQL .= " and lp.calculated_country_code in ($sCountryCodesSQL)";
1600                                         if ($sOrderBySQL) $sSQL .= "order by ".$sOrderBySQL." asc";
1601                                         if ($this->iOffset) $sSQL .= " offset $this->iOffset";
1602                                         $sSQL .= " limit $this->iLimit";
1603                                         if (CONST_Debug) var_dump($sSQL);
1604                                         $aClassPlaceIDs = array_merge($aClassPlaceIDs, chksql($this->oDB->getCol($sSQL)));
1605                                     } else {
1606                                         if (isset($aSearch['fRadius']) && $aSearch['fRadius']) $fRange = $aSearch['fRadius'];
1607
1608                                         $sOrderBySQL = '';
1609                                         if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.geometry)";
1610                                         else $sOrderBySQL = "ST_Distance(l.geometry, f.geometry)";
1611
1612                                         $sSQL = "SELECT distinct l.place_id".($sOrderBysSQL?','.$sOrderBysSQL:'');
1613                                         $sSQL .= " FROM placex as l, placex as f ";
1614                                         $sSQL .= " WHERE f.place_id in ($sPlaceIDs) ";
1615                                         $sSQL .= "  AND ST_DWithin(l.geometry, f.centroid, $fRange) ";
1616                                         $sSQL .= "  AND l.class='".$aSearch['sClass']."' ";
1617                                         $sSQL .= "  AND l.type='".$aSearch['sType']."' ";
1618                                         if (sizeof($this->aExcludePlaceIDs)) {
1619                                             $sSQL .= " AND l.place_id not in (".join(',', $this->aExcludePlaceIDs).")";
1620                                         }
1621                                         if ($sCountryCodesSQL) $sSQL .= " AND l.calculated_country_code in ($sCountryCodesSQL)";
1622                                         if ($sOrderBy) $sSQL .= "ORDER BY ".$OrderBysSQL." ASC";
1623                                         if ($this->iOffset) $sSQL .= " OFFSET $this->iOffset";
1624                                         $sSQL .= " limit $this->iLimit";
1625                                         if (CONST_Debug) var_dump($sSQL);
1626                                         $aClassPlaceIDs = array_merge($aClassPlaceIDs, chksql($this->oDB->getCol($sSQL)));
1627                                     }
1628                                 }
1629                             }
1630                             $aPlaceIDs = $aClassPlaceIDs;
1631                         }
1632                     }
1633
1634                     if (CONST_Debug) {
1635                         echo "<br><b>Place IDs:</b> ";
1636                         var_Dump($aPlaceIDs);
1637                     }
1638
1639                     foreach ($aPlaceIDs as $iPlaceID) {
1640                         // array for placeID => -1 | Tiger housenumber
1641                         $aResultPlaceIDs[$iPlaceID] = $searchedHousenumber;
1642                     }
1643                     if ($iQueryLoop > 20) break;
1644                 }
1645
1646                 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs) && ($this->iMinAddressRank != 0 || $this->iMaxAddressRank != 30)) {
1647                     // Need to verify passes rank limits before dropping out of the loop (yuk!)
1648                     // reduces the number of place ids, like a filter
1649                     // rank_address is 30 for interpolated housenumbers
1650                     $sSQL = "SELECT place_id ";
1651                     $sSQL .= "FROM placex ";
1652                     $sSQL .= "WHERE place_id in (".join(',', array_keys($aResultPlaceIDs)).") ";
1653                     $sSQL .= "  AND (";
1654                     $sSQL .= "         placex.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank ";
1655                     if (14 >= $this->iMinAddressRank && 14 <= $this->iMaxAddressRank) {
1656                         $sSQL .= "     OR (extratags->'place') = 'city'";
1657                     }
1658                     if ($this->aAddressRankList) {
1659                         $sSQL .= "     OR placex.rank_address in (".join(',', $this->aAddressRankList).")";
1660                     }
1661                     if (CONST_Use_US_Tiger_Data) {
1662                         $sSQL .= "  ) ";
1663                         $sSQL .= "UNION ";
1664                         $sSQL .= "  SELECT place_id ";
1665                         $sSQL .= "  FROM location_property_tiger ";
1666                         $sSQL .= "  WHERE place_id in (".join(',', array_keys($aResultPlaceIDs)).") ";
1667                         $sSQL .= "    AND (30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
1668                         if ($this->aAddressRankList) $sSQL .= " OR 30 in (".join(',', $this->aAddressRankList).")";
1669                     }
1670                     $sSQL .= ") UNION ";
1671                     $sSQL .= "  SELECT place_id ";
1672                     $sSQL .= "  FROM location_property_osmline ";
1673                     $sSQL .= "  WHERE place_id in (".join(',', array_keys($aResultPlaceIDs)).")";
1674                     $sSQL .= "    AND (30 between $this->iMinAddressRank and $this->iMaxAddressRank)";
1675                     if (CONST_Debug) var_dump($sSQL);
1676                     $aFilteredPlaceIDs = chksql($this->oDB->getCol($sSQL));
1677                     $tempIDs = array();
1678                     foreach ($aFilteredPlaceIDs as $placeID) {
1679                         $tempIDs[$placeID] = $aResultPlaceIDs[$placeID];  //assign housenumber to placeID
1680                     }
1681                     $aResultPlaceIDs = $tempIDs;
1682                 }
1683
1684                 //exit;
1685                 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs)) break;
1686                 if ($iGroupLoop > 4) break;
1687                 if ($iQueryLoop > 30) break;
1688             }
1689
1690             // Did we find anything?
1691             if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs)) {
1692                 $aSearchResults = $this->getDetails($aResultPlaceIDs);
1693             }
1694         } else {
1695             // Just interpret as a reverse geocode
1696             $oReverse = new ReverseGeocode($this->oDB);
1697             $oReverse->setZoom(18);
1698
1699             $aLookup = $oReverse->lookup(
1700                 (float)$this->aNearPoint[0],
1701                 (float)$this->aNearPoint[1],
1702                 false
1703             );
1704
1705             if (CONST_Debug) var_dump("Reverse search", $aLookup);
1706
1707             if ($aLookup['place_id']) {
1708                 $aSearchResults = $this->getDetails(array($aLookup['place_id'] => -1));
1709                 $aResultPlaceIDs[$aLookup['place_id']] = -1;
1710             } else {
1711                 $aSearchResults = array();
1712             }
1713         }
1714
1715         // No results? Done
1716         if (!sizeof($aSearchResults)) {
1717             if ($this->bFallback) {
1718                 if ($this->fallbackStructuredQuery()) {
1719                     return $this->lookup();
1720                 }
1721             }
1722
1723             return array();
1724         }
1725
1726         $aClassType = getClassTypesWithImportance();
1727         $aRecheckWords = preg_split('/\b[\s,\\-]*/u', $sQuery);
1728         foreach ($aRecheckWords as $i => $sWord) {
1729             if (!preg_match('/\pL/', $sWord)) unset($aRecheckWords[$i]);
1730         }
1731
1732         if (CONST_Debug) {
1733             echo '<i>Recheck words:<\i>';
1734             var_dump($aRecheckWords);
1735         }
1736
1737         $oPlaceLookup = new PlaceLookup($this->oDB);
1738         $oPlaceLookup->setIncludePolygonAsPoints($this->bIncludePolygonAsPoints);
1739         $oPlaceLookup->setIncludePolygonAsText($this->bIncludePolygonAsText);
1740         $oPlaceLookup->setIncludePolygonAsGeoJSON($this->bIncludePolygonAsGeoJSON);
1741         $oPlaceLookup->setIncludePolygonAsKML($this->bIncludePolygonAsKML);
1742         $oPlaceLookup->setIncludePolygonAsSVG($this->bIncludePolygonAsSVG);
1743         $oPlaceLookup->setPolygonSimplificationThreshold($this->fPolygonSimplificationThreshold);
1744
1745         foreach ($aSearchResults as $iResNum => $aResult) {
1746             // Default
1747             $fDiameter = getResultDiameter($aResult);
1748
1749             $aOutlineResult = $oPlaceLookup->getOutlines($aResult['place_id'], $aResult['lon'], $aResult['lat'], $fDiameter/2);
1750             if ($aOutlineResult) {
1751                 $aResult = array_merge($aResult, $aOutlineResult);
1752             }
1753             
1754             if ($aResult['extra_place'] == 'city') {
1755                 $aResult['class'] = 'place';
1756                 $aResult['type'] = 'city';
1757                 $aResult['rank_search'] = 16;
1758             }
1759
1760             // Is there an icon set for this type of result?
1761             if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1762                 && $aClassType[$aResult['class'].':'.$aResult['type']]['icon']
1763             ) {
1764                 $aResult['icon'] = CONST_Website_BaseURL.'images/mapicons/'.$aClassType[$aResult['class'].':'.$aResult['type']]['icon'].'.p.20.png';
1765             }
1766
1767             if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'])
1768                 && $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label']
1769             ) {
1770                 $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'];
1771             } elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1772                 && $aClassType[$aResult['class'].':'.$aResult['type']]['label']
1773             ) {
1774                 $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type']]['label'];
1775             }
1776             // if tag '&addressdetails=1' is set in query
1777             if ($this->bIncludeAddressDetails) {
1778                 // getAddressDetails() is defined in lib.php and uses the SQL function get_addressdata in functions.sql
1779                 $aResult['address'] = getAddressDetails($this->oDB, $sLanguagePrefArraySQL, $aResult['place_id'], $aResult['country_code'], $aResultPlaceIDs[$aResult['place_id']]);
1780                 if ($aResult['extra_place'] == 'city' && !isset($aResult['address']['city'])) {
1781                     $aResult['address'] = array_merge(array('city' => array_values($aResult['address'])[0]), $aResult['address']);
1782                 }
1783             }
1784
1785             if ($this->bIncludeExtraTags) {
1786                 if ($aResult['extra']) {
1787                     $aResult['sExtraTags'] = json_decode($aResult['extra']);
1788                 } else {
1789                     $aResult['sExtraTags'] = (object) array();
1790                 }
1791             }
1792
1793             if ($this->bIncludeNameDetails) {
1794                 if ($aResult['names']) {
1795                     $aResult['sNameDetails'] = json_decode($aResult['names']);
1796                 } else {
1797                     $aResult['sNameDetails'] = (object) array();
1798                 }
1799             }
1800
1801             // Adjust importance for the number of exact string matches in the result
1802             $aResult['importance'] = max(0.001, $aResult['importance']);
1803             $iCountWords = 0;
1804             $sAddress = $aResult['langaddress'];
1805             foreach ($aRecheckWords as $i => $sWord) {
1806                 if (stripos($sAddress, $sWord)!==false) {
1807                     $iCountWords++;
1808                     if (preg_match("/(^|,)\s*".preg_quote($sWord, '/')."\s*(,|$)/", $sAddress)) $iCountWords += 0.1;
1809                 }
1810             }
1811
1812             $aResult['importance'] = $aResult['importance'] + ($iCountWords*0.1); // 0.1 is a completely arbitrary number but something in the range 0.1 to 0.5 would seem right
1813
1814             $aResult['name'] = $aResult['langaddress'];
1815             // secondary ordering (for results with same importance (the smaller the better):
1816             // - approximate importance of address parts
1817             $aResult['foundorder'] = -$aResult['addressimportance']/10;
1818             // - number of exact matches from the query
1819             if (isset($this->exactMatchCache[$aResult['place_id']])) {
1820                 $aResult['foundorder'] -= $this->exactMatchCache[$aResult['place_id']];
1821             } elseif (isset($this->exactMatchCache[$aResult['parent_place_id']])) {
1822                 $aResult['foundorder'] -= $this->exactMatchCache[$aResult['parent_place_id']];
1823             }
1824             // - importance of the class/type
1825             if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['importance'])
1826                 && $aClassType[$aResult['class'].':'.$aResult['type']]['importance']
1827             ) {
1828                 $aResult['foundorder'] += 0.0001 * $aClassType[$aResult['class'].':'.$aResult['type']]['importance'];
1829             } else {
1830                 $aResult['foundorder'] += 0.01;
1831             }
1832             if (CONST_Debug) var_dump($aResult);
1833             $aSearchResults[$iResNum] = $aResult;
1834         }
1835         uasort($aSearchResults, 'byImportance');
1836
1837         $aOSMIDDone = array();
1838         $aClassTypeNameDone = array();
1839         $aToFilter = $aSearchResults;
1840         $aSearchResults = array();
1841
1842         $bFirst = true;
1843         foreach ($aToFilter as $iResNum => $aResult) {
1844             $this->aExcludePlaceIDs[$aResult['place_id']] = $aResult['place_id'];
1845             if ($bFirst) {
1846                 $fLat = $aResult['lat'];
1847                 $fLon = $aResult['lon'];
1848                 if (isset($aResult['zoom'])) $iZoom = $aResult['zoom'];
1849                 $bFirst = false;
1850             }
1851             if (!$this->bDeDupe || (!isset($aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']])
1852                 && !isset($aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']]))
1853             ) {
1854                 $aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']] = true;
1855                 $aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']] = true;
1856                 $aSearchResults[] = $aResult;
1857             }
1858
1859             // Absolute limit on number of results
1860             if (sizeof($aSearchResults) >= $this->iFinalLimit) break;
1861         }
1862
1863         return $aSearchResults;
1864     } // end lookup()
1865 } // end class