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