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