<?php /*

 ocPortal
 Copyright (c) ocProducts, 2004-2012

 See text/EN/licence.txt for full licencing information.


 NOTE TO PROGRAMMERS:
   Do not edit this file. If you need to make changes, save your changed file to the appropriate *_custom folder
   **** If you ignore this advice, then your website upgrades (e.g. for bug fixes) will likely kill your changes ****

*/

/**
 * @license		http://opensource.org/licenses/cpal_1.0 Common Public Attribution License
 * @copyright	ocProducts Ltd
 * @package		core
 */

/*EXTRA FUNCTIONS: var_export*/

/**
 * Standard code module initialisation function.
 */
function init__database_search()
{
	$GLOBALS['TOTAL_RESULTS']=0;
}

/**
 * Highlight keywords in an extracted portion of a piece of text.
 *
 * @param  string			What was searched
 * @param  array			List of words searched
 * @return string			Highlighted portion
 */
function generate_text_summary($_temp_summary,$words_searched)
{
	require_code('xhtml');

	$summary='';
	
	global $SEARCH__CONTENT_BITS;

	$_temp_summary_lower=strtolower($_temp_summary);

	// Add in some highlighting direct to XHTML
	$all_occurrences=array();
	foreach (is_null($SEARCH__CONTENT_BITS)?array():$SEARCH__CONTENT_BITS as $content_bit)
	{
		if ($content_bit=='') continue;

		$last_pos=0;
		$content_bit_pos=0;
		do
		{
			$content_bit_matched=$content_bit;
			if (strtoupper($content_bit)==$content_bit) // all upper case so don't want case sensitive
			{
				$content_bit_pos=strpos($_temp_summary,$content_bit,$last_pos);
			} else
			{
				$content_bit_pos=strpos($_temp_summary_lower,strtolower($content_bit),$last_pos);
				if (strpos($content_bit,'-')!==false)
				{
					$content_bit_pos_2=strpos($_temp_summary_lower,strtolower(str_replace('-','',$content_bit)),$last_pos);
					if (($content_bit_pos_2!==false) && (($content_bit_pos===false) || ($content_bit_pos_2<$content_bit_pos)))
					{
						$content_bit_pos=$content_bit_pos_2;
						$content_bit_matched=str_replace('-','',$content_bit);
					}
				}
			}

			if ($content_bit_pos!==false)
			{
				$last_gt=strrpos(substr($_temp_summary,0,$content_bit_pos),'>');
				$last_lt=strrpos(substr($_temp_summary,0,$content_bit_pos),'<');

				if (($last_gt!==false) && ($last_gt>$last_lt))
				{
					$extra_pre='<span class="comcode_highlight">';
					$extra_post='</span>';
					$_temp_summary=substr($_temp_summary,0,$content_bit_pos).
										$extra_pre.
										substr($_temp_summary,$content_bit_pos,strlen($content_bit_matched)).
										$extra_post.
										substr($_temp_summary,$content_bit_pos+strlen($content_bit_matched));
					$_temp_summary_lower=strtolower($_temp_summary);
					$last_pos=$content_bit_pos+strlen($extra_pre)+strlen($content_bit_matched)+strlen($extra_post);

					// Adjust all stores occurrence offsets
					foreach ($all_occurrences as $i=>$occ)
					{
						if ($occ[0]>$last_pos)
						{
							$all_occurrences[$i][0]+=strlen($extra_pre)+strlen($extra_post);
							$all_occurrences[$i][1]+=strlen($extra_pre)+strlen($extra_post);
						}
						elseif ($occ[0]>$content_bit_pos)
						{
							$all_occurrences[$i][0]+=strlen($extra_pre);
							$all_occurrences[$i][1]+=strlen($extra_pre);
						}
					}

					$all_occurrences[]=array($content_bit_pos,$last_pos);
				} else
				{
					$last_pos=$content_bit_pos+strlen($content_bit_matched);
				}
			}
		}
		while ($content_bit_pos!==false);
	}

	if (strlen($_temp_summary)<500)
	{
		$summary=$_temp_summary;
	} else
	{
		// Find optimal position
		$len=strlen($_temp_summary);
		$best_yet=0;
		$best_pos_min=250;
		$best_pos_max=250;
		if (count($all_occurrences)<60) // Only bother doing this if we need to dig for the keyword
		{
			for ($i=250;$i<$len-250;$i++) // Move window along all possible positions
			{
				$count=0;
				$i_pre=$i-250;
				$i_post=$i+250;
				foreach ($all_occurrences as $occ)
				{
					$occ_pre=$occ[0];
					$occ_post=$occ[1];
					if (($occ_pre>=$i_pre) && ($occ_pre<=$i_post) && ($occ_post>=$i_pre) && ($occ_post<=$i_post))
					{
						$count++;

						if ($count>5) break; // Good enough
					}
				}
				if (($count>$best_yet) || (($best_yet==$count) && ($i-500<$best_pos_min)))
				{
					if ($best_yet==$count)
					{
						$best_pos_max=$i;
					} else
					{
						$best_yet=$count;
						$best_pos_min=$i;
						$best_pos_max=$i;
					}

					if ($count>5) break; // Good enough
				}
			}
			$best_pos=intval(floatval($best_pos_min+$best_pos_max)/2.0)-250; // Move it from center pos, to where we want to start from
		} else
		{
			$best_pos=0;
		}

		// Render (with ellipses if required)
		if (false)
		{ // Far far too slow
			$summary=xhtml_substr($_temp_summary,$best_pos,min(500,$len-$best_pos),true,true);
		} else
		{
			$summary=substr($_temp_summary,$best_pos,min(500,$len-$best_pos));
			$summary=xhtmlise_html($summary,true);
			if ($best_pos>0) $summary='&hellip;'.$summary;
			if ($best_pos+500<strlen($_temp_summary)) $summary.='&hellip;';
		}
	}
	
	return $summary;
}

/**
 * Server opensearch requests.
 */
function opensearch_script()
{
	header("Cache-Control: no-cache, must-revalidate"); // HTTP/1.1
	header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); // Date in the past
	
	if (!has_actual_page_access(get_member(),'search')) return; // No access
	
	$type=get_param('type','misc');
	switch ($type)
	{
		// Make a search suggestion (like Google Suggest)
		case 'suggest':
			header('Content-type: text/plain; charset='.get_charset());
			$request=get_param('request',false,true);
			if (strlen($request)<3) return;
			
			$suggestions=array();
			$q='SELECT s_primary,COUNT(*) as cnt,MAX(s_num_results) AS s_num_results FROM '.get_table_prefix().'searches_logged WHERE ';
			if ((db_has_full_text($GLOBALS['SITE_DB']->connection_read)) && (method_exists($GLOBALS['SITE_DB']->static_ob,'db_has_full_text_boolean')) && ($GLOBALS['SITE_DB']->static_ob->db_has_full_text_boolean()) && (!is_under_radar($request)))
			{
				$q.=preg_replace('#\?(.*)#','s_primary${1}',db_full_text_assemble($request,false));
			} else
			{
				$q.='s_primary LIKE \''./*ideally we would put an % in front, but too slow*/db_encode_like($request.'%').'\'';
			}
			$q.=' AND s_primary NOT LIKE \''.db_encode_like('%<%').'\' AND '.db_string_not_equal_to('s_primary','').' GROUP BY s_primary ORDER BY cnt DESC';
			$past_searches=$GLOBALS['SITE_DB']->query($q,20);
			foreach ($past_searches as $search)
			{
				if ($search['cnt']>5)
					$suggestions[$search['s_primary']]=$search['s_num_results'];
			}
			
			require_lang('search');
			
			@ini_set('ocproducts.xss_detect','0');

			// JSON format
			echo '['.chr(10);
				// Original request
				echo '"'.php_addslashes($request).'",'.chr(10);

				// Suggestions
				echo '[';
					foreach (array_keys($suggestions) as $i=>$suggestion)
					{
						if ($i!=0) echo ',';
						echo '"'.php_addslashes($suggestion).'"';
					}
				echo '],'.chr(10);

				// Descriptions of suggestions
				echo '[';
					foreach (array_values($suggestions) as $i=>$suggestion)
					{
						if ($i!=0) echo ',';
						echo '"'.php_addslashes(do_lang('NUM_RESULTS',integer_format($suggestion))).'"';
					}
				echo '],'.chr(10);

				// URLs to search suggestions
				$filter=get_param('filter','');
				$filter_map=array();
				if ($filter!='')
				{
					foreach (explode(':',$filter) as $f)
					{
						if ($f!='')
						{
							$parts=explode('=',$f,2);
							if (count($parts)==1) $parts=array($parts[0],'1');
							$filter_map[$parts[0]]=$parts[1];
						}
					}
				}
				echo '[';
					foreach (array_keys($suggestions) as $i=>$suggestion)
					{
						if ($i!=0) echo ',';
						$map=array('page'=>'search','type'=>'results','content'=>$suggestion)+$filter_map;
						$_search_url=build_url($map,get_param('zone',get_module_zone('search')));
						$search_url=$_search_url->evaluate();
						echo '"'.php_addslashes($search_url).'"';
					}
				echo ']'.chr(10);
			echo ']'.chr(10);
			break;

		// Provide details about the site search engine
		default:
			//header('Content-Type: application/opensearchdescription+xml');
			header('Content-Type: text/xml');
			$tpl=do_template('OPENSEARCH',array('DESCRIPTION'=>get_option('description')));
			$tpl->evaluate_echo();
			break;
	}
}

/**
 * Build up a submitter search clause, taking into account members, authors, usernames, and usergroups.
 *
 * @param  ?ID_TEXT		The field name for member IDs (NULL: Cannot match against member IDs)
 * @param  ?MEMBER		Member ID (NULL: Unknown, so cannot search)
 * @param  ID_TEXT		Author
 * @param  ?ID_TEXT		The field name for authors (NULL: Cannot match against member IDs)
 * @return ?string		An SQL fragment (NULL: block query)
 */
function build_search_submitter_clauses($member_field_name,$member_id,$author,$author_field_name=NULL)
{
	$clauses='';

	// Member ID
	if ((!is_null($member_id)) && (!is_null($member_field_name)))
	{
		if ($clauses!='') $clauses.=' OR ';
		$clauses.=$member_field_name.'='.strval($member_id);
	}
	
	// Groups
	if ((!is_null($member_field_name)) && ($author!=''))
	{
		$all_usergroups=$GLOBALS['FORUM_DRIVER']->get_usergroup_list(true);
		foreach ($all_usergroups as $usergroup=>$usergroup_name)
		{
			if ($usergroup_name==$author)
			{
				$members_in_group=$GLOBALS['FORUM_DRIVER']->member_group_query(array($usergroup),50);
				if (count($members_in_group)<50) // Let's be reasonable with how long the SQL could get!
				{
					foreach (array_keys($members_in_group) as $group_member_id)
					{
						if ($clauses!='') $clauses.=' OR ';
						$clauses.=$member_field_name.'='.strval($group_member_id);
					}
				}
				break;
			}
		}
	}
	
	// Author
	if ((!is_null($author_field_name)) && ($author!=''))
	{
		if ($clauses!='') $clauses.=' OR ';
		$clauses.=db_string_equal_to($author_field_name,$author);
	}
	
	if ($clauses=='')
	{
		if ($author!='') return NULL; // Query should never succeed
		
		return '';
	}
	
	return ' AND ('.$clauses.')';
}

/**
 * Get special SQL from POSTed parameters for a catalogue search field that is to be exact-matched.
 *
 * @param  array			The row for the field to input
 * @param  integer		We're processing for the ith row
 * @param  ID_TEXT		Table type
 * @set short long
 * @param  ?string		Search term (NULL: lookup from environment)
 * @return ?array			Tuple of SQL details (array: extra trans fields to search, array: extra plain fields to search, string: an extra table segment for a join, string: the name of the field to use as a title, if this is the title, extra WHERE clause stuff) (NULL: nothing special)
 */
function exact_match_sql($row,$i,$type='short',$param=NULL)
{
	$table=' LEFT JOIN '.$GLOBALS['SITE_DB']->get_table_prefix().'catalogue_efv_'.$type.' f'.strval($i).' ON (f'.strval($i).'.ce_id=r.id AND f'.strval($i).'.cf_id='.strval($row['id']).')';
	$search_field='f'.strval($i).'.cv_value';
	if (is_null($param)) $param=get_param('option_'.strval($row['id']),'');
	$where_clause='';
	if ($param!='')
	{
		if ($type=='float')
		{
			$where_clause.=$search_field.'='.$param;
		}
		elseif ($type=='integer')
		{
			$where_clause.=$search_field.'='.$param;
		} else
		{
			$where_clause=db_string_equal_to($search_field,$param);
		}
	}
	return array(array(),array('f'.strval($i).'.cv_value'),$table,$search_field,$where_clause);
}

/**
 * Get special SQL from POSTed parameters for a catalogue search field for a multi-input field that is to be exact-matched.
 *
 * @param  array			The row for the field to input
 * @param  integer		We're processing for the ith row
 * @param  ID_TEXT		Table type
 * @set short long
 * @param  ?string		Search term (NULL: lookup from environment)
 * @return ?array			Tuple of SQL details (array: extra trans fields to search, array: extra plain fields to search, string: an extra table segment for a join, string: the name of the field to use as a title, if this is the title, extra WHERE clause stuff) (NULL: nothing special)
 */
function nl_delim_match_sql($row,$i,$type='short',$param=NULL)
{
	$table=' LEFT JOIN '.$GLOBALS['SITE_DB']->get_table_prefix().'catalogue_efv_'.$type.' f'.strval($i).' ON (f'.strval($i).'.ce_id=r.id AND f'.strval($i).'.cf_id='.strval($row['id']).')';
	$search_field='f'.strval($i).'.cv_value';
	if (is_null($param)) $param=get_param('option_'.strval($row['id']),'');
	$where_clause='';
	if ($param!='') $where_clause='('.$search_field.' LIKE \''.db_encode_like($param).'\' OR '.$search_field.' LIKE \''.db_encode_like('%'.chr(10).$param).'\' OR '.$search_field.' LIKE \''.db_encode_like($param.chr(10).'%').'\' OR '.$search_field.' LIKE \''.db_encode_like('%'.chr(10).$param.chr(10).'%').'\')';
	return array(array(),array('f'.strval($i).'.cv_value'),$table,$search_field,$where_clause);
}

/**
 * Get some rows, queried from the database according to the search parameters.
 *
 * @param  ?ID_TEXT		The META type used by our content (NULL: Cannot support META search)
 * @param  ?ID_TEXT		The name of the field that retrieved META IDs will relate to (NULL: Cannot support META search)
 * @param  string			Search string
 * @param  boolean		Whether to do a boolean search.
 * @param  ID_TEXT		Boolean operator
 * @set OR AND
 * @param  boolean		Whether to only do a META (tags) search
 * @param  ID_TEXT		Order direction
 * @param  integer		Start position in total results
 * @param  integer		Maximum results to return in total
 * @param  boolean		Whether to only search titles (as opposed to both titles and content)
 * @param  ID_TEXT		The table name
 * @param  array			The translateable fields to search over (or an ! which is skipped). The first of these must be the title field or an '!'; if it is '!' then the title field will be the first raw-field
 * @param  string			The WHERE clause
 * @param  string			The WHERE clause that applies specifically for content (this will be duplicated to check against multiple fields). ? refers to the yet-unknown field name
 * @param  ID_TEXT		What to order by
 * @param  string			What to select
 * @param  ?array			The non-translateable fields to search over (NULL: there are none)
 * @param  ?string		The permission module to check category access for (NULL: none)
 * @param  ?string		The field that specifies the permissions ID to check category access for (NULL: none)
 * @param  boolean		Whether the permissions field is a string
 * @return array			The rows found
 */
function get_search_rows($meta_type,$meta_id_field,$content,$boolean_search,$boolean_operator,$only_search_meta,$direction,$max,$start,$only_titles,$table,$fields,$where_clause,$content_where,$order,$select='*',$raw_fields=NULL,$permissions_module=NULL,$permissions_field=NULL,$permissions_field_is_string=false)
{
	if (substr($where_clause,0,5)==' AND ') $where_clause=substr($where_clause,5);
	if (substr($where_clause,-5)==' AND ') $where_clause=substr($where_clause,0,strlen($where_clause)-5);
	$where_alternative_matches=array();
	
	$had_limit_imposed=false;

	if ((!is_null($permissions_module)) && (!$GLOBALS['FORUM_DRIVER']->is_super_admin(get_member())))
	{
		$g_or=_get_where_clause_groups(get_member());

// this destroys mysqls query optimiser by forcing complexed OR's into the join, so we'll do this in PHP code
//		$table.=' LEFT JOIN '.$GLOBALS['SITE_DB']->get_table_prefix().'group_category_access z ON ('.db_string_equal_to('z.module_the_name',$permissions_module).' AND z.category_name='.$permissions_field.(($g_or!='')?(' AND '.str_replace('group_id','z.group_id',$g_or)):'').')';
//		$where_clause.=' AND ';
//		$where_clause.='z.category_name IS NOT NULL';

		$cat_access=list_to_map('category_name',$GLOBALS['SITE_DB']->query('SELECT category_name FROM '.$GLOBALS['SITE_DB']->get_table_prefix().'group_category_access WHERE '.db_string_equal_to('module_the_name',$permissions_module).(($g_or!='')?(' AND ('.$g_or.')'):'')));
	}

	if (($only_titles) && (array_key_exists(0,$fields)) && ($fields[0]=='')) return array();

	if (is_null($raw_fields)) $raw_fields=array();

	$db=(substr($table,0,2)!='f_')?$GLOBALS['SITE_DB']:$GLOBALS['FORUM_DB'];

	// This is so for example catalogue_entries.php can use brackets in it's table specifier whilst avoiding the table prefix after the first bracket. A bit weird, but that's our convention and it does save a small amount of typing
	$table_clause=$db->get_table_prefix().(($table[0]=='(')?(substr($table,1)):$table);
	if ($table[0]=='(') $table_clause='('.$table_clause;

	$t_rows=array();
	$t_count=0;

	// Rating ordering, via special encoding
	if (substr($order,0,7)=='_rating')
	{
		list(,$rating_type,$meta_rating_id_field)=explode(':',$order);
		$select.=',(SELECT AVG(rating) FROM '.get_table_prefix().'rating WHERE '.db_string_equal_to('rating_for_type',$rating_type).' AND rating_for_id='.$meta_rating_id_field.') AS compound_rating';
		$order='compound_rating';
	}

	$translate_join_type=(get_value('alternate_search_join_type')==='1')?'LEFT JOIN':'JOIN';

	// Defined-keywords/tags search
	if ((get_param_integer('keep_just_show_query',0)==0) && (!is_null($meta_type)) && ($content!=''))
	{
		$keywords_where=preg_replace('#\?#','tm.text_original',build_content_where($content,$boolean_search,$boolean_operator,true));

		if ($keywords_where!='')
		{
			if ($meta_id_field=='the_zone:the_page') // Special case
			{
				$meta_join='m.meta_for_id=CONCAT(r.the_zone,\':\',r.the_page)';
			} else
			{
				$meta_join='m.meta_for_id=r.'.$meta_id_field;
			}
			$extra_join='';
			foreach ($fields as $i=>$field) // Translatable fields present in 'select'
			{
				if (($field=='') || ($field=='!') || (strpos($select,'t1.text_original')===false)) continue;

				$extra_join.=' '.$translate_join_type.' '.$db->get_table_prefix().'translate t'.strval($i).' ON t'.strval($i).'.id='.$field.' AND '.db_string_equal_to('t'.strval($i).'.language',user_lang());
			}
			if (!db_has_subqueries($db->connection_read) || true /* Forced this old code to run because the "optimisation" does not work for larger result sets */)
			{
				$_keywords_query=$table_clause.' LEFT JOIN '.$db->get_table_prefix().'seo_meta m ON ('.db_string_equal_to('m.meta_for_type',$meta_type).' AND '.$meta_join.') '.$translate_join_type.' '.$db->get_table_prefix().'translate tm ON tm.id=m.meta_keywords AND '.db_string_equal_to('tm.language',user_lang()).$extra_join;
				$_keywords_query.=' WHERE '.$keywords_where;
				$_keywords_query.=(($where_clause!='')?(' AND '.$where_clause):'');
			} else
			{
				$_keywords_query=/*str_replace(' LEFT JOIN ',' JOIN ',*/$table_clause/*)*/.' LEFT JOIN '.$db->get_table_prefix().'seo_meta m ON ('.db_string_equal_to('m.meta_for_type',$meta_type).' AND '.$meta_join.') '.$translate_join_type.' '.$db->get_table_prefix().'translate tm ON tm.id=m.meta_keywords AND '.db_string_equal_to('tm.language',user_lang()).$extra_join;
				$_keywords_query.=' WHERE '.$keywords_where;
				$_keywords_query.=(($where_clause!='')?(' AND tm.id IN (SELECT m.id FROM '.$table_clause.' LEFT JOIN '.$db->get_table_prefix().'seo_meta m ON ('.db_string_equal_to('m.meta_for_type',$meta_type).' AND '.$meta_join.') '.$translate_join_type.' '.$db->get_table_prefix().'translate tm ON tm.id=m.meta_keywords AND '.db_string_equal_to('tm.language',user_lang()).' WHERE '.$where_clause.' AND '.$keywords_where.')'):'');
			}
			$keywords_query='SELECT '.$select.' FROM '.$_keywords_query;
			$_count_query_keywords_search='SELECT COUNT(*) FROM '.$_keywords_query;

			$group_by_ok=(can_arbitrary_groupby() && $meta_id_field==='id');
			
			$keywords_query.=($group_by_ok?' GROUP BY r.id':'');

			if ($order!='')
			{
				$keywords_query.=' ORDER BY '.$order;
				if ($direction=='DESC') $keywords_query.=' DESC';
			}

			if ($group_by_ok)
			{
				$_count_query_keywords_search=str_replace('COUNT(*)','COUNT(DISTINCT r.id)',$_count_query_keywords_search);
			}

			$t_keyword_search_rows_count=$db->query_value_null_ok_full($_count_query_keywords_search);
			if ($t_keyword_search_rows_count>500) // Too much to sort in memory, so we will just put up with overlapping content types that aren't sorted together right
			{
				$t_keyword_search_rows=$db->query($keywords_query,$max+$start);
				$had_limit_imposed=true;
			} else
			{
				$t_keyword_search_rows=$db->query($keywords_query);
			}
			$t_count+=$t_keyword_search_rows_count;
			$t_rows=array_merge($t_rows,$t_keyword_search_rows);
		} else $_count_query_keywords_search=NULL;
	} else $_count_query_keywords_search=NULL;

	$orig_table_clause=$table_clause;

	// Main content search
	if (!$only_search_meta)
	{
		if (($content_where!='') || (preg_match('#t\d+\.text_original#',$where_clause)!=0) || (preg_match('#t\d+\.text_original#',$select)!=0))
		{
			// Each of the fields represents an 'OR' match, so we put it together into a list ($where_alternative_matches) of specifiers for each. Hopefully we will 'UNION' them rather than 'OR' them as it is much more efficient in terms of table index usage
			
			$where_alternative_matches=array();
			foreach ($fields as $i=>$field) // Referenced fields in where condition must result in the shared table clause having a reference to the translate for that
			{
				if ((strpos($select,'t'.strval($i).'.text_original')!==false) || (strpos($where_clause,'t'.strval($i).'.text_original')!==false))
				{
					$tc_add=' '.$translate_join_type.' '.$db->get_table_prefix().'translate t'.strval($i).' ON t'.strval($i).'.id='.$field.' AND '.db_string_equal_to('t'.strval($i).'.language',user_lang());
					$orig_table_clause.=$tc_add;
				}
			}
			foreach ($fields as $i=>$field) // Translatable fields
			{
				if (($field=='') || ($field=='!')) continue;

				if ($field==$order) $order='t'.$i.'.text_original'; // Ah, remap to the textual equivalent then

				$tc_add=' '.$translate_join_type.' '.$db->get_table_prefix().'translate t'.strval($i).' ON t'.strval($i).'.id='.$field.' AND '.db_string_equal_to('t'.strval($i).'.language',user_lang());
				if (strpos($orig_table_clause,$tc_add)!==false) $tc_add='';

				if (($only_titles) && ($i!=0)) break;

				$where_clause_2=preg_replace('#\?#','t'.strval($i).'.text_original',$content_where);
				$where_clause_3=$where_clause;
				if (($table=='f_members') && (substr($field,0,6)=='field_') && (db_has_subqueries($db->connection_read)))
					$where_clause_3.=(($where_clause=='')?'':' AND ').'NOT EXISTS (SELECT * FROM '.$db->get_table_prefix().'f_cpf_perms cpfp WHERE cpfp.member_id=r.id AND cpfp.field_id='.substr($field,6).' AND cpfp.guest_view=0)';

				if (($order=='') && (db_has_expression_ordering($db->connection_read)) && ($content_where!=''))
				{
					$_select=preg_replace('#\?#','t'.strval($i).'.text_original',$content_where).' AS contextual_relevance';
				} else
				{
					$_select='';
				}
				$_table_clause=$orig_table_clause.$tc_add;

				$where_alternative_matches[]=array($where_clause_2,$where_clause_3,$_select,$_table_clause,'t'.strval($i));
			}
			if ($content_where!='') // Non-translatable fields
			{
				foreach ($raw_fields as $i=>$field)
				{
					if (($only_titles) && ($i!=0)) break;

					$where_clause_2=preg_replace('#\?#',$field,$content_where);
					$where_clause_3=$where_clause;
					if (($table=='f_members') && (substr($field,0,6)=='field_') && (db_has_subqueries($db->connection_read)))
						$where_clause_3.=(($where_clause=='')?'':' AND ').'NOT EXISTS (SELECT * FROM '.$db->get_table_prefix().'f_cpf_perms cpfp WHERE cpfp.member_id=r.id AND cpfp.field_id='.substr($field,6).' AND cpfp.guest_view=0)';

					if (($order=='') && (db_has_expression_ordering($db->connection_read)) && ($content_where!=''))
					{
						$_select=preg_replace('#\?#',$field,$content_where).' AS contextual_relevance';
					} else
					{
						$_select='';
					}

					$_table_clause=$orig_table_clause;

					$where_alternative_matches[]=array($where_clause_2,$where_clause_3,$_select,$_table_clause,NULL);
				}
			}
		}

		if (count($where_alternative_matches)==0)
		{
			$where_alternative_matches[]=array($where_clause,'','',$table_clause,NULL);
		} else
		{
			if (($order=='') && (db_has_expression_ordering($db->connection_read)) && ($content_where!=''))
			{
				$order='contextual_relevance DESC';
			}
		}

		// Work out main query
		global $SITE_INFO;
		if (((isset($SITE_INFO['mysql_old'])) && ($SITE_INFO['mysql_old']=='1')) || ((!isset($SITE_INFO['mysql_old'])) && (is_file(get_file_base().'/mysql_old'))))
		{
			$_query='';
			foreach ($where_alternative_matches as $parts)
			{
				list($where_clause_2,$where_clause_3,$_select,,)=$parts;
				$where_clause_3=$where_clause_2.(($where_clause_3=='')?'':((($where_clause_2=='')?'':' AND ').$where_clause_3));

				$select.=(($_select=='')?'':',').$_select;
				$_query.=(($where_clause_3!='')?((($_query=='')?' WHERE ':' OR ').$where_clause_3):'');
			}
			$query='SELECT '.$select.' FROM '.$table_clause.$_query;
		} else
		{
			$query='';
			foreach ($where_alternative_matches as $parts) // We UNION them, because doing OR's on MATCH's is insanely slow in MySQL (sometimes I hate SQL...)
			{
				list($where_clause_2,$where_clause_3,$_select,$_table_clause,$tid)=$parts;

				if ($query!='') $query.=' UNION ';
				
				if ((!db_has_subqueries($db->connection_read)) || (is_null($tid)) || ($content_where=='') || true)
				{
					$where_clause_3=$where_clause_2.(($where_clause_3=='')?'':((($where_clause_2=='')?'':' AND ').$where_clause_3));

					$query.='SELECT '.$select.(($_select=='')?'':',').$_select.' FROM '.$_table_clause.(($where_clause_3=='')?'':' WHERE '.$where_clause_3);
				} else // Optimised using subqueries. We need the fulltext search to run first to avoid non-bounded joins over potentially huge tables
				{
					$query.='SELECT '.$select.(($_select=='')?'':',').$_select.' FROM './*str_replace(' LEFT JOIN ',' JOIN ',*/$_table_clause/*)*/;
					if (($where_clause_2!='') || ($where_clause_3!=''))
					{
						$query.=' WHERE '.$where_clause_2;
						$query.=(($where_clause_3!='')?((($where_clause_2=='')?'':' AND ').$tid.'.id IN (SELECT '.$tid.'.id FROM '.$_table_clause.' WHERE '.$where_clause_2.(($where_clause_3=='')?'':((($where_clause_2=='')?'':' AND ').$where_clause_3)).')'):'');
					}
				}
			}
		}
		// Work out COUNT(*) query using one of a few possible methods. It's not efficient and stops us doing proper merge-sorting between content types (and possible not accurate - if we use an efficient but non-deduping COUNT strategy) if we have to use this, so we only do it if there are too many rows to fetch in one go.
		$_query='';
		if (((isset($SITE_INFO['mysql_old'])) && ($SITE_INFO['mysql_old']=='1')) || ((!isset($SITE_INFO['mysql_old'])) && (is_file(get_file_base().'/mysql_old'))) || (strpos(get_db_type(),'mysql')===false))
		{
			foreach ($where_alternative_matches as $parts)
			{
				list($where_clause_2,$where_clause_3,,$_table_clause,$tid)=$parts;

				if ((!db_has_subqueries($db->connection_read)) || (is_null($tid)) || ($content_where=='') || true)
				{
					$where_clause_3=$where_clause_2.(($where_clause_3=='')?'':((($where_clause_2=='')?'':' AND ').$where_clause_3));

					$_query.=(($where_clause_3!='')?((($_query=='')?' WHERE ':' OR ').$where_clause_3):'');
				} else
				{
					if (($where_clause_2!='') || ($where_clause_3!=''))
					{
						$_query.=(($_query=='')?' WHERE ':' OR ').$where_clause_2;
						$_query.=(($where_clause_3!='')?((($where_clause_2=='')?'':' AND ').$tid.'.id IN (SELECT '.$tid.'.id FROM '.$_table_clause.' WHERE '.$where_clause_2.(($where_clause_3=='')?'':((($where_clause_2=='')?'':' AND ').$where_clause_3)).')'):'');
					}
				}
			}
			$_count_query_main_search='SELECT COUNT(*) FROM '.$table_clause.$_query;
		} else // This is inaccurate (does ot filter dupes from each +'d query) but much more efficient on MySQL
		{
			foreach ($where_alternative_matches as $parts) // We "+" them, because doing OR's on MATCH's is insanely slow in MySQL (sometimes I hate SQL...)
			{
				list($where_clause_2,$where_clause_3,$_select,$_table_clause,$tid)=$parts;

				if ($_query!='') $_query.='+';

				if ((!db_has_subqueries($db->connection_read)) || (is_null($tid)) || ($content_where=='') || true)
				{
					$where_clause_3=$where_clause_2.(($where_clause_3=='')?'':((($where_clause_2=='')?'':' AND ').$where_clause_3));

					$_query.='(SELECT COUNT(*) FROM '.$_table_clause.(($where_clause_3=='')?'':' WHERE '.$where_clause_3).')';
				} else // Optimised using subqueries. We need the fulltext search to run first to avoid non-bounded joins over potentially huge tables
				{
					$_query.='(SELECT COUNT(*) FROM './*str_replace(' LEFT JOIN ',' JOIN ',*/$_table_clause/*)*/;
					if (($where_clause_2!='') || ($where_clause_3!=''))
					{
						$_query.=' WHERE '.$where_clause_2;
						$_query.=(($where_clause_3!='')?((($where_clause_2=='')?'':' AND ').$tid.'.id IN (SELECT '.$tid.'.id FROM '.$_table_clause.' WHERE '.$where_clause_2.(($where_clause_3=='')?'':((($where_clause_2=='')?'':' AND ').$where_clause_3)).')'):'');
					}
					$_query.=')';
				}
			}
			$_count_query_main_search='SELECT ('.$_query.')';
		}

		$group_by_ok=(can_arbitrary_groupby() && $meta_id_field==='id');

		$query.=($group_by_ok?' GROUP BY r.id':'');

		if ($order!='')
		{
			$query.=' ORDER BY '.$order;
			if (($direction=='DESC') && (substr($order,-4)!=' ASC') && (substr($order,-5)!=' DESC')) $query.=' DESC';
		}

		if (get_param_integer('keep_show_query',0)==1)
		{
			attach_message($query,'inform');
		}
		if (get_param_integer('keep_just_show_query',0)==1)
		{
			@ini_set('ocproducts.xss_detect','0');
			header('Content-type: text/plain; charset='.get_charset());
			exit($query);
		}

		if ($group_by_ok)
		{
			$_count_query_main_search=str_replace('COUNT(*)','COUNT(DISTINCT r.id)',$_count_query_main_search);
		}

		$t_main_search_rows_count=$db->query_value_null_ok_full($_count_query_main_search);
		if ($t_main_search_rows_count>500) // Too much to sort in memory, so we will just put up with overlapping content types that aren't sorted together right
		{
			$t_main_search_rows=$db->query($query,$max+$start,NULL,false,true);
			$had_limit_imposed=true;
		} else
		{
			$t_main_search_rows=$db->query($query,NULL,NULL,false,true);
		}
		$t_count+=$t_main_search_rows_count;
		$t_rows=array_merge($t_rows,$t_main_search_rows);
	} else
	{
		$t_main_search_rows=array();
	}

	// Clean results and return
	// NB: We don't use the count_query's any more (except when using huge data sets, see above), because you can't actually just add them because they overlap. So instead we fetch all results and throw some away.

	$t_rows=array_merge($t_rows,$t_main_search_rows);
	if (count($t_rows)>0)
	{
		$t_rows_new=array();
		if ((array_key_exists('id',$t_rows[0])) || (array_key_exists('_primary_id',$t_rows[0])))
		{
			$done=array();
			foreach ($t_rows as $t_row)
			{
				if (array_key_exists('id',$t_row))
				{
					if (array_key_exists($t_row['id'],$done)) continue;
					$done[$t_row['id']]=1;
				}
				elseif (array_key_exists('_primary_id',$t_row))
				{
					if (array_key_exists($t_row['_primary_id'],$done)) continue;
					$done[$t_row['_primary_id']]=1;
				}
				$t_rows_new[]=$t_row;
			}
		} else
		{
			foreach ($t_rows as $t_row)
			{
				unset($t_row['contextual_relevance']);
				foreach ($t_rows_new as $_t_row)
				{
					if (($_t_row==$t_row) || ((array_key_exists('id',$t_row)) && (array_key_exists('id',$_t_row)) && (!array_key_exists('_primary_id',$t_row)) && (!array_key_exists('_primary_id',$_t_row)) && ($t_row['id']==$_t_row['id'])) || ((array_key_exists('_primary_id',$t_row)) && (array_key_exists('_primary_id',$_t_row)) && ($t_row['_primary_id']==$_t_row['_primary_id'])))
						continue 2;
				}
				$t_rows_new[]=$t_row;
			}
		}
		$t_rows=$t_rows_new;
	}
	if (get_param_integer('keep_show_query',0)==1)
	{
		if ((array_key_exists(0,$t_rows)) && (array_key_exists('id',$t_rows[0])))
		{
			$results=var_export(array_unique(collapse_1d_complexity('id',$t_rows)),true);
		} else
		{
			$results=var_export($t_rows,true);
		}
		attach_message(do_lang('_RESULTS').': '.$results,'inform');
	}

	if (isset($cat_access))
	{
		$before=count($t_rows);
		foreach ($t_rows as $i=>$row)
		{
			if (!array_key_exists(strval($row[$permissions_field]),$cat_access)) unset($t_rows[$i]);
		}
	}
	$final_result_rows=$t_rows;
	if (!$had_limit_imposed) // More accurate, as filtered for dupes
		$t_count=count($t_rows);
	$GLOBALS['TOTAL_RESULTS']+=$t_count;
	array_splice($final_result_rows,$max*2+$start); // We return more than max in case our search hook does some extra in-code filtering (Catalogues, Comcode pages). It shouldn't really but sometimes it has to, and it certainly shouldn't filter more than 50%. Also so our overall ordering can be better.

	return $final_result_rows;
}

/**
 * Take a search string and find boolean search parameters from it.
 *
 * @param  string			The search string
 * @return array			Words to search under the boolean operator, words that must be included, words that must not be included.
 */
function _boolean_search_prepare($search_filter)
{
	$search_filter=str_replace('?','_',$search_filter);
	$search_filter=str_replace('*','%',$search_filter);

	$content_explode=explode(' ',$search_filter);

	$body_words=array();
	$include_words=array();
	$disclude_words=array();
	for ($i=0;$i<count($content_explode);$i++)
	{
		$word=trim($content_explode[$i]);
		if (($word=='') || ($word=='+') || ($word=='-')) continue;

		// Handle quotes
		if ($word[0]=='"')
		{
			$i++;
			while ($i<count($content_explode))
			{
				$word2=trim($content_explode[$i]);
				if ($word2!='')
				{
					$word.=' '.$word2;
				}
				if (substr($word2,-1)=='"')
				{
					break;
				} else
				{
					$i++;
				}
			}
			if (substr($word,-1)!='"') $word.='"';
			$word=substr($word,1,strlen($word)-2);
		}

		if ($word[0]=='+') $include_words[]=$word;
		elseif ($word[0]=='-') $disclude_words[]=$word;
		else $body_words[]=$word;
	}
	
	return array($body_words,$include_words,$disclude_words);
}

/**
 * Perform a database-style in-memory boolean search on single item.
 *
 * @param  array			A map of POST data in search-form style. May contain 'only_titles', 'content' (the critical one!) and 'conjunctive_operator'
 * @param  string			The title to try and match
 * @param  ?string		The post to try and match (NULL: not used)
 * @return boolean		Whether we have a match
 */
function in_memory_search_match($filter,$title,$post=NULL)
{
	if ((!array_key_exists('content',$filter)) || ($filter['content']=='')) return true;

	$search_filter=$filter['content'];
	if (((array_key_exists('only_titles',$filter)) && ($filter['only_titles']==1)) || (is_null($post)))
	{
		$context=$title;
	} else
	{
		$context=$title.' '.$post;
	}

	$boolean_operator=array_key_exists('conjunctive_operator',$filter)?$filter['conjunctive_operator']:'OR';

	list($body_words,$include_words,$disclude_words)=_boolean_search_prepare($search_filter);
	foreach ($include_words as $word)
	{
		if (!simulated_wildcard_match($context,$word)) return false;
	}
	foreach ($disclude_words as $word)
	{
		if (simulated_wildcard_match($context,$word)) return false;
	}
	if ($boolean_operator=='OR')
	{
		$count=0;
		foreach ($body_words as $word)
		{
			if (simulated_wildcard_match($context,$word)) $count++;
		}
		if ($count==0) return false;
	} else
	{
		foreach ($body_words as $word)
		{
			if (!simulated_wildcard_match($context,$word)) return false;
		}
	}
	return true;
}

/**
 * Find whether a phrase is too small for fulltext search.
 *
 * @param  string		The phrase
 * @return boolean	Whether it is
 */
function is_under_radar($test)
{
	if (get_value('disable_boolean_search')==='1') return false;

	static $min_word_length=NULL;
	if (is_null($min_word_length))
	{
		$min_word_length=4;
		if (substr(get_db_type(),0,5)=='mysql')
		{
			$_min_word_length=$GLOBALS['SITE_DB']->query('SHOW VARIABLES LIKE \'ft_min_word_len\'',NULL,NULL);
			if (array_key_exists(0,$_min_word_length))
				$min_word_length=intval($_min_word_length[0]['Value']);
		}
	}

	return ((strlen($test)<$min_word_length) && ($test!=''));
}

/**
 * Build a fulltext query WHERE clause from given content.
 *
 * @param  string			The search content
 * @param  boolean		Whether it's a boolean search
 * @param  string			Boolean operation to use
 * @set    AND OR
 * @param  boolean		Whether we can assume we require full coverage
 * @return string			WHERE clause
 */
function build_content_where($content,$boolean_search,&$boolean_operator,$full_coverage=false)
{
	$content=str_replace('?','',$content);
	
	list($body_words,$include_words,$disclude_words)=_boolean_search_prepare($content);

	$under_radar=false;
	if ((is_under_radar($content)) && ($content!='')) $under_radar=true;
	if (($under_radar) || ($boolean_search) || (!db_has_full_text($GLOBALS['SITE_DB']->connection_read)))
	{
		if (!in_array(strtoupper($boolean_operator),array('AND','OR'))) log_hack_attack_and_exit('ORDERBY_HACK');

		if ($content=='')
		{
			$content_where='';
		} else
		{
			if ((db_has_full_text($GLOBALS['SITE_DB']->connection_read)) && (method_exists($GLOBALS['SITE_DB']->static_ob,'db_has_full_text_boolean')) && ($GLOBALS['SITE_DB']->static_ob->db_has_full_text_boolean()) && (!$under_radar))
			{
				$content_where=db_full_text_assemble($content,true);
			} else
			{
				$content_where=db_like_assemble($content,$boolean_operator,$full_coverage);
				if ($content_where=='') $content_where='1=1';
			}
		}
	} else
	{
		if ($content=='')
		{
			$content_where='';
		} else
		{
			$content_where=db_full_text_assemble($content,false);
		}
		$boolean_operator='OR';
	}

	return $content_where;
}

/**
 * Generate SQL for a boolean search.
 *
 * @param  string			Boolean search string
 * @param  string			Boolean operator to use
 * @set    AND OR
 * @param  boolean		Whether we can assume we require full coverage
 * @return string			The SQL (may be blank)
 */
function db_like_assemble($content,$boolean_operator='AND',$full_coverage=false)
{
	list($body_words,$include_words,$disclude_words)=_boolean_search_prepare($content);

	$fc_before=$full_coverage?'':'%';
	$fc_after=$full_coverage?'':'%';

	$body_where='';
	foreach ($body_words as $word)
	{
		if ($body_where!='') $body_where.=' '.$boolean_operator.' ';
		if ((strtoupper($word)==$word) && (method_exists($GLOBALS['SITE_DB']->static_ob,'db_has_collate_settings')) && ($GLOBALS['SITE_DB']->static_ob->db_has_collate_settings($GLOBALS['SITE_DB']->connection_read)) && (!is_numeric($word)))
		{
			$body_where.='CONVERT(? USING latin1) LIKE _latin1\''.db_encode_like($fc_before.$word.$fc_after).'\' COLLATE latin1_general_cs';
		} else
		{
			$body_where.='? LIKE \''.db_encode_like($fc_before.$word.$fc_after).'\'';
		}
	}
	$include_where='';
	foreach ($include_words as $word)
	{
		if ($include_where!='') $include_where.=' AND ';
		if ((strtoupper($word)==$word) && (method_exists($GLOBALS['SITE_DB']->static_ob,'db_has_collate_settings')) && ($GLOBALS['SITE_DB']->static_ob->db_has_collate_settings($GLOBALS['SITE_DB']->connection_read)) && (!is_numeric($word)))
		{
			$include_where.='CONVERT(? USING latin1) LIKE _latin1\''.db_encode_like($fc_before.$word.$fc_after).'\' COLLATE latin1_general_cs';
		} else
		{
			$include_where.='? LIKE \''.db_encode_like($fc_before.$word.$fc_after).'\'';
		}
	}
	$disclude_where='';
	foreach ($disclude_words as $word)
	{
		if ($disclude_where!='') $disclude_where.=' AND ';
		if ((strtoupper($word)==$word) && (method_exists($GLOBALS['SITE_DB']->static_ob,'db_has_collate_settings')) && ($GLOBALS['SITE_DB']->static_ob->db_has_collate_settings($GLOBALS['SITE_DB']->connection_read)) && (!is_numeric($word)))
		{
			$disclude_where.='CONVERT(? USING latin1) NOT LIKE _latin1\''.db_encode_like($fc_before.$word.$fc_after).'\' COLLATE latin1_general_cs';
		} else
		{
			$disclude_where.='? NOT LIKE \''.db_encode_like($fc_before.$word.$fc_after).'\'';
		}
	}
	$content_where='';
	if ($body_where!='') $content_where.='('.$body_where.')';
	if ($include_where!='')
	{
		if ($content_where!='') $content_where.=' AND ';
		$content_where.='('.$include_where.')';
	}
	if ($disclude_where!='')
	{
		if ($content_where!='') $content_where.=' AND ';
		$content_where.='('.$disclude_where.')';
	}
	return $content_where;
}

/**
 * Sort search results as returned by the search hook.
 *
 * @param  array		Search results from the search hook, assumed already sorted
 * @param  array		Existing array of results (originally starts blank)
 * @return array		Sorted results
 * @param  string		Sort direction
 * @set    ASC DESC
 */
function sort_search_results($hook_results,$results,$direction)
{
	// Do a merge sort
	$results_position=0;
	$done_all=false;
	foreach ($hook_results as $i=>$result)
	{
		while (true)
		{
			if (!array_key_exists($results_position,$results)) // If we've run off the end of our current results
			{
				$results=array_merge($results,array_slice($hook_results,$i));
				$done_all=true;
				break;
			}
			if ((array_key_exists('orderer',$result)) && (array_key_exists('orderer',$results[$results_position])) && ((($direction=='ASC') && ($result['orderer']<=$results[$results_position]['orderer'])) || (($direction=='DESC') && ($result['orderer']>=$results[$results_position]['orderer'])))) // If it definitely beats, put in front. If it's unknown (no orderer on one - which is very common) it has to go on the end so FIFO is preserved
			{
				$results=array_merge(array_slice($results,0,$results_position),array($result),array_slice($results,$results_position));
				break;
			}
			$results_position++;
		}
		if ($done_all)
		{
			break;
		}
	}

	return $results;
}

/**
 * Build a templated list of the given search results, for viewing.
 *
 * @param  array			Search results
 * @param  integer		Start index
 * @param  integer		Maximum index
 * @param  string			Sort direction
 * @set    ASC DESC
 * @param  boolean		Whether this is a general search, rather than a search for a specific result-type (such as all members)
 * @return tempcode		Interface
 */
function build_search_results_interface($results,$start,$max,$direction,$general_search=false)
{
	$out=new ocp_tempcode();
	$i=0;
	global $CATALOGUE_ENTRIES_BUILDUP;
	$CATALOGUE_ENTRIES_BUILDUP=array();
	$tabular_results=array();
	foreach ($results as $result)
	{
		if (array_key_exists('restricted',$result)) continue; // This has been blanked out due to insufficient access permissions or some other reason

		if ($i>=$start+$max) break;

		if ($i>=$start)
		{
			if (array_key_exists('template',$result))
			{
				$rendered_result=$result['template'];
			} else
			{
				if ((!$general_search) && (get_param_integer('force_non_tabular',0)==0) && (method_exists($result['object'],'render_tabular')))
				{
					$rendered_result=$result['object']->render_tabular($result['data']);
				} else
				{
					$rendered_result=$result['object']->render($result['data']);
				}
			}
			if (!is_null($rendered_result))
			{
				if (is_array($rendered_result))
				{
					$class=get_class($result['object']);
					if (!array_key_exists($class,$tabular_results)) $tabular_results[$class]=array();
					$tabular_results[$class][]=$rendered_result;
				} else
				{
					$out->attach(do_template('SEARCH_RESULT',array('_GUID'=>'47da093f9ace87819e246f0cec1402a9','CONTENT'=>$rendered_result)));
				}
			}
		}
		$i++;
	}
	foreach ($tabular_results as $tabular_type=>$types_results)
	{
		// Normalisation process
		$ultimate_field_map=array();
		foreach ($types_results as $r)
			$ultimate_field_map+=$r;
		//ksort($ultimate_field_map);
		$ultimate_field_map=array_keys($ultimate_field_map);
		foreach ($types_results as $i=>$r)
		{
			$r2d2=array();
			foreach ($ultimate_field_map as $key)
			{
				if (!array_key_exists($key,$r)) $r[$key]='';
				$r2d2[$key]=$r[$key];
			}
			//ksort($r);
			$r=$r2d2;
			$types_results[$i]=array('R'=>$r);
		}

		// Output
		$out->attach(do_template('SEARCH_RESULT_TABLE',array('HEADERS'=>$ultimate_field_map,'ROWS'=>$types_results)));
	}
	if (count($CATALOGUE_ENTRIES_BUILDUP)!=0)
	{
		global $SEARCH_CATALOGUE_ENTRIES_CATALOGUES;
		foreach ($CATALOGUE_ENTRIES_BUILDUP as $catalogue_name=>$catalogue_entries)
		{
			$tpl_set=$catalogue_name;
			$buildup=get_catalogue_category_entry_buildup(NULL,$catalogue_name,$SEARCH_CATALOGUE_ENTRIES_CATALOGUES[$catalogue_name],'SEARCH',$tpl_set,NULL,NULL,NULL,NULL,NULL,false,$catalogue_entries);
			$out->attach(do_template('SEARCH_RESULT_CATALOGUE_ENTRIES',array('_GUID'=>'dd0045ac291275b2d822d40b28182a28','BUILDUP'=>$buildup[0],'NAME'=>$catalogue_name,'TITLE'=>get_translated_text($SEARCH_CATALOGUE_ENTRIES_CATALOGUES[$catalogue_name]['c_title']))));
		}
	}

	$GLOBALS['META_DATA']+=array(
		'opensearch_totalresults'=>strval($i),
		'opensearch_startindex'=>strval($start),
		'opensearch_itemsperpage'=>strval($max),
	);

	$SEARCH__CONTENT_BITS=NULL;

	return $out;
}
