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