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