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