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