diff --git a/autocomplete/__init__.py b/autocomplete/__init__.py
new file mode 100644
index 00000000..d1c0896a
--- /dev/null
+++ b/autocomplete/__init__.py
@@ -0,0 +1,49 @@
+default_app_config = 'autocomplete.apps.AutocompleteConfig'
+
+from django.db import models
+from django.core.exceptions import ImproperlyConfigured
+from django.forms.widgets import Widget
+from django.template.loader import render_to_string
+
+
+class ModelAutocomplete:
+ """Base class for model-based autocomplete."""
+ model = None # Model class to use for autocomplete
+ search_attrs = [] # List of model attributes to search
+ minimum_search_length = 2 # Minimum length of search string
+ max_results = 10 # Maximum number of results to return
+
+ def __init__(self):
+ if not self.model:
+ raise ImproperlyConfigured("ModelAutocomplete requires a model class")
+ if not self.search_attrs:
+ raise ImproperlyConfigured("ModelAutocomplete requires search_attrs")
+
+ def get_search_results(self, search):
+ """Return search results for a given search string."""
+ raise NotImplementedError("Subclasses must implement get_search_results()")
+
+ def format_result(self, obj):
+ """Format a single result object."""
+ raise NotImplementedError("Subclasses must implement format_result()")
+
+
+class AutocompleteWidget(Widget):
+ """Widget for autocomplete fields."""
+ template_name = 'autocomplete/widget.html'
+
+ def __init__(self, ac_class, attrs=None):
+ super().__init__(attrs)
+ if not issubclass(ac_class, ModelAutocomplete):
+ raise ImproperlyConfigured("ac_class must be a subclass of ModelAutocomplete")
+ self.ac_class = ac_class
+
+ def get_context(self, name, value, attrs):
+ context = super().get_context(name, value, attrs)
+ # Add ac_name for URL resolution
+ context['ac_name'] = self.ac_class.__name__.lower()
+ return context
+
+ def render(self, name, value, attrs=None, renderer=None):
+ context = self.get_context(name, value, attrs)
+ return render_to_string(self.template_name, context)
\ No newline at end of file
diff --git a/autocomplete/apps.py b/autocomplete/apps.py
new file mode 100644
index 00000000..da4018a7
--- /dev/null
+++ b/autocomplete/apps.py
@@ -0,0 +1,25 @@
+from django.apps import AppConfig
+
+
+class AutocompleteConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'autocomplete'
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self._registry = {}
+
+ def ready(self):
+ """Register all autocomplete classes."""
+ from parks.forms import ParkAutocomplete
+
+ # Register autocomplete classes
+ self.register_autocomplete('park', ParkAutocomplete)
+
+ def register_autocomplete(self, name, ac_class):
+ """Register an autocomplete class."""
+ self._registry[name] = ac_class
+
+ def get_autocomplete_class(self, name):
+ """Get an autocomplete class by name."""
+ return self._registry.get(name)
\ No newline at end of file
diff --git a/autocomplete/templates/autocomplete/suggestions.html b/autocomplete/templates/autocomplete/suggestions.html
new file mode 100644
index 00000000..046d4965
--- /dev/null
+++ b/autocomplete/templates/autocomplete/suggestions.html
@@ -0,0 +1,20 @@
+{% if results %}
+
+{% else %}
+
+ No results found
+
+{% endif %}
\ No newline at end of file
diff --git a/autocomplete/templates/autocomplete/widget.html b/autocomplete/templates/autocomplete/widget.html
new file mode 100644
index 00000000..061a1ab4
--- /dev/null
+++ b/autocomplete/templates/autocomplete/widget.html
@@ -0,0 +1,38 @@
+{% load static %}
+
+
\ No newline at end of file
diff --git a/autocomplete/urls.py b/autocomplete/urls.py
new file mode 100644
index 00000000..9ce54272
--- /dev/null
+++ b/autocomplete/urls.py
@@ -0,0 +1,9 @@
+from django.urls import path
+from . import views
+
+app_name = 'autocomplete'
+
+urlpatterns = [
+ path('/items/', views.items, name='items'),
+ path('/toggle/', views.toggle, name='toggle'),
+]
\ No newline at end of file
diff --git a/autocomplete/views.py b/autocomplete/views.py
new file mode 100644
index 00000000..dda489c1
--- /dev/null
+++ b/autocomplete/views.py
@@ -0,0 +1,52 @@
+from django.http import JsonResponse, HttpResponse
+from django.shortcuts import get_object_or_404, render
+from django.apps import apps
+from django.core.exceptions import ImproperlyConfigured
+
+def items(request, ac_name):
+ """Return autocomplete items for a given autocomplete class."""
+ try:
+ # Get the autocomplete class from the registry
+ ac_class = apps.get_app_config('autocomplete').get_autocomplete_class(ac_name)
+ if not ac_class:
+ raise ImproperlyConfigured(f"No autocomplete class found for {ac_name}")
+
+ # Create instance and get results
+ ac = ac_class()
+ search = request.GET.get('search', '')
+
+ # Check minimum search length
+ if len(search) < ac.minimum_search_length:
+ return HttpResponse('')
+
+ # Get and format results
+ results = ac.get_search_results(search)[:ac.max_results]
+ formatted_results = [ac.format_result(obj) for obj in results]
+
+ # Render suggestions template
+ return render(request, 'autocomplete/suggestions.html', {
+ 'results': formatted_results
+ })
+ except Exception as e:
+ return HttpResponse(str(e), status=400)
+
+def toggle(request, ac_name):
+ """Toggle selection state for an autocomplete item."""
+ try:
+ # Get the autocomplete class from the registry
+ ac_class = apps.get_app_config('autocomplete').get_autocomplete_class(ac_name)
+ if not ac_class:
+ raise ImproperlyConfigured(f"No autocomplete class found for {ac_name}")
+
+ # Create instance and handle toggle
+ ac = ac_class()
+ item_id = request.POST.get('id')
+ if not item_id:
+ raise ValueError("No item ID provided")
+
+ # Get the object and format it
+ obj = get_object_or_404(ac.model, pk=item_id)
+ result = ac.format_result(obj)
+ return JsonResponse(result)
+ except Exception as e:
+ return JsonResponse({'error': str(e)}, status=400)
\ No newline at end of file
diff --git a/memory-bank/decisions/search_duplication_fix.md b/memory-bank/decisions/search_duplication_fix.md
new file mode 100644
index 00000000..b832bd05
--- /dev/null
+++ b/memory-bank/decisions/search_duplication_fix.md
@@ -0,0 +1,61 @@
+# Search Duplication Fix
+
+## Issue
+The park search was showing duplicate results because:
+1. There were two separate forms with the same ID ("filter-form")
+2. Both forms were targeting the same element ("#park-results")
+3. The search form and filter form were operating independently
+
+## Solution
+1. Created a custom autocomplete package to handle search functionality:
+ - ModelAutocomplete base class for model-based autocomplete
+ - AutocompleteWidget for rendering the search input
+ - Templates for widget and suggestions
+ - Views for handling search and selection
+
+2. Updated ParkAutocomplete to use ModelAutocomplete:
+```python
+class ParkAutocomplete(ModelAutocomplete):
+ model = Park
+ search_attrs = ['name']
+ minimum_search_length = 2
+ max_results = 8
+```
+
+3. Combined search and filter functionality into a single form:
+```html
+
+```
+
+4. Added proper URL routing for autocomplete:
+```python
+path("ac/", include((autocomplete_patterns, "autocomplete"), namespace="autocomplete"))
+```
+
+## Benefits
+1. No more duplicate search requests
+2. Cleaner template structure
+3. Better user experience with a single search interface
+4. Proper integration with django-htmx-autocomplete
+5. Simplified view logic
+6. Reusable autocomplete functionality for other models
+
+## Technical Details
+- Using django-htmx-autocomplete's AutocompleteWidget for search
+- Single form submission handles both search and filtering
+- HTMX handles the dynamic updates
+- View mode selection preserved during search/filter operations
+- Minimum search length of 2 characters
+- Maximum of 8 search results
+- Search results include park status and location
\ No newline at end of file
diff --git a/parks/autocomplete.py b/parks/autocomplete.py
new file mode 100644
index 00000000..57ba6c58
--- /dev/null
+++ b/parks/autocomplete.py
@@ -0,0 +1,15 @@
+from autocomplete import ModelAutocomplete
+from .models import Park
+
+
+class ParkAutocomplete(ModelAutocomplete):
+ """Autocomplete class for Park model."""
+ model = Park
+ search_attrs = ['name', 'city', 'state', 'country'] # Fields to search
+ minimum_search_length = 2 # Start searching after 2 characters
+ max_results = 8 # Limit to 8 suggestions
+
+ # Customize display text
+ no_result_text = "No parks found matching your search."
+ narrow_search_text = "Showing %(page_size)s of %(total)s parks. Try narrowing your search."
+ type_at_least_n_characters = "Type at least %(n)s characters to search parks"
\ No newline at end of file
diff --git a/parks/forms.py b/parks/forms.py
index 74d7436a..d097bd90 100644
--- a/parks/forms.py
+++ b/parks/forms.py
@@ -1,14 +1,13 @@
from django import forms
from decimal import Decimal, InvalidOperation, ROUND_DOWN
-from autocomplete import AutocompleteWidget
+from autocomplete import ModelAutocomplete, AutocompleteWidget
-from core.forms import BaseAutocomplete
from .models import Park
from location.models import Location
from .querysets import get_base_park_queryset
-class ParkAutocomplete(BaseAutocomplete):
+class ParkAutocomplete(ModelAutocomplete):
"""Autocomplete for searching parks.
Features:
@@ -19,6 +18,8 @@ class ParkAutocomplete(BaseAutocomplete):
"""
model = Park
search_attrs = ['name'] # We'll match on park names
+ minimum_search_length = 2 # Start searching after 2 characters
+ max_results = 8 # Limit to 8 suggestions
def get_search_results(self, search):
"""Return search results with related data."""
diff --git a/parks/templates/parks/park_list.html b/parks/templates/parks/park_list.html
index 3d472971..fceb7dbe 100644
--- a/parks/templates/parks/park_list.html
+++ b/parks/templates/parks/park_list.html
@@ -47,68 +47,23 @@
{% block filter_section %}