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