diff --git a/mo/db.py b/mo/db.py
index 2ecf598842c882bc43caedc891a6eaf24ff85cc8..f331c08a1e0ebd02fe779e8a5652f01450bf5793 100644
--- a/mo/db.py
+++ b/mo/db.py
@@ -129,6 +129,9 @@ class Place(Base):
     def get_code(self):
         return self.code or '#' + str(self.place_id)
 
+    def name_or_id(self):
+        return self.name or '#' + str(self.place_id)
+
     def can_have_child(self):
         return len(PlaceType.choices(level=self.level + 1)) > 0
 
diff --git a/mo/web/org.py b/mo/web/org.py
index 377f6ab34ef03b416f75c114c107405cb4ef6502..66d2e12fea8295dee2ba5d2c51b23c9d85b16048 100644
--- a/mo/web/org.py
+++ b/mo/web/org.py
@@ -1,6 +1,7 @@
 from collections import defaultdict
 from dataclasses import dataclass, field
 from flask import render_template, redirect, url_for, request, flash, g
+import re
 from sqlalchemy import and_, or_, tuple_, not_
 from sqlalchemy.orm import aliased, joinedload
 from typing import List, Set, Optional, Tuple, DefaultDict
@@ -39,15 +40,11 @@ def org_index():
         else:
             flash(f'Místo s kódem {code} neexistuje', 'danger')
 
-    if 'uid' in request.args:
-        try:
-            uid = int(request.args['uid'])
-            user = mo.users.user_by_uid(uid)
-            if user is not None:
-                return redirect(user_url(user))
-            flash(f'Uživatel s ID {uid} neexistuje', 'danger')
-        except ValueError:
-            flash('ID uživatele musí být číslo', 'danger')
+    # Univerzální hledátko pro správce
+    if 'search' in request.args:
+        search_result = magic_search(request.args['search'])
+        if search_result is not None:
+            return search_result
 
     # Soutěže, ke kterým máme nějakou roli
     sess = db.get_session()
@@ -220,6 +217,35 @@ school_export_columns = (
 )
 
 
+def magic_search(query: str):
+    search = re.fullmatch(r'([cpru])(\d{1,9})', request.args['search'])
+    if not search:
+        flash('Chybná syntaxe dotazu', 'danger')
+        return None
+    what, id = search[1], int(search[2])
+    sess = db.get_session()
+
+    if what == 'c':
+        contest = sess.query(db.Contest).get(id)
+        if contest is not None:
+            return redirect(url_for('org_contest', ct_id=id))
+    elif what == 'p':
+        place = sess.query(db.Place).get(id)
+        if place is not None:
+            return redirect(url_for('org_place', id=id))
+    elif what == 'r':
+        round = sess.query(db.Round).get(id)
+        if round is not None:
+            return redirect(url_for('org_round', round_id=id))
+    elif what == 'u':
+        user = mo.users.user_by_uid(id)
+        if user is not None:
+            return redirect(user_url(user))
+
+    flash('Nenalezeno', 'danger')
+    return None
+
+
 @app.route('/org/export/schools')
 def org_export_schools():
     sess = db.get_session()
diff --git a/mo/web/org_contest.py b/mo/web/org_contest.py
index 552a569e0d0166feee1183de3b9367fe1fec764a..e81e6af9597b557348488a1b527ef3882ba9b336 100644
--- a/mo/web/org_contest.py
+++ b/mo/web/org_contest.py
@@ -1012,12 +1012,6 @@ def org_submit_list(ct_id: int, user_id: int, task_id: int, site_id: Optional[in
     )
 
 
-class SubmitEditForm(FlaskForm):
-    note = wtforms.TextAreaField("Poznámka pro účastníka", description="Viditelná účastníkovi po uzavření kola", render_kw={"rows": 8, 'autofocus': True})
-    org_note = wtforms.TextAreaField("Interní poznámka", description="Viditelná jen organizátorům", render_kw={"rows": 8})
-    submit = wtforms.SubmitField("Uložit")
-
-
 @app.route('/org/contest/c/<int:ct_id>/paper/<int:paper_id>/<filename>', endpoint='org_submit_paper')
 @app.route('/org/contest/c/<int:ct_id>/site/<int:site_id>/paper/<int:paper_id>/<filename>', endpoint='org_submit_paper')
 @app.route('/org/contest/c/<int:ct_id>/paper/orig/<int:paper_id>/<filename>', endpoint='org_submit_paper_orig')
diff --git a/mo/web/org_place.py b/mo/web/org_place.py
index a04ca1885318c67824bc10a462612a42ff34e12c..a37e46bd16d191ea64427ed5d555912c4e7cb4f8 100644
--- a/mo/web/org_place.py
+++ b/mo/web/org_place.py
@@ -2,6 +2,7 @@ from flask import render_template, g, redirect, url_for, flash, request
 from flask_wtf import FlaskForm
 import locale
 from markupsafe import Markup
+from sqlalchemy import func, and_
 from sqlalchemy.orm import joinedload
 from typing import List, Optional
 import werkzeug.exceptions
@@ -17,6 +18,11 @@ import mo.web.fields as mo_fields
 import wtforms.validators as validators
 
 
+class PlaceSearchForm(FlaskForm):
+    query = mo_fields.String(render_kw={'autofocus': True, 'placeholder': 'Kód nebo části názvu'})
+    submit = wtforms.SubmitField('Hledat')
+
+
 @app.route('/org/place/<int:id>/')
 def org_place(id: int):
     sess = db.get_session()
@@ -25,6 +31,44 @@ def org_place(id: int):
     if not place:
         raise werkzeug.exceptions.NotFound()
 
+    # Formulář nemá side-efekty, takže to může být GET bez CSRF.
+    search_form = PlaceSearchForm(request.args, meta={'csrf': False})
+    found_places = None
+    search_failed = False
+    search_limited = False
+    if 'submit' in request.args and search_form.validate():
+        query = search_form.query.data
+        query_words = query.split()
+
+        if len(query_words) == 1:
+            found = db.get_place_by_code(query_words[0])
+            if found is not None:
+                flash('Nalezeno toto místo', 'info')
+                return redirect(url_for('org_place', id=found.place_id))
+
+        if len(query_words) > 0 and '%' not in query:
+            max_places = 100
+            place_q = (sess.query(db.Place)
+                       .filter(db.Place.place_id != place.place_id))
+            for qw in query_words:
+                place_q = place_q.filter(func.lower(db.f_unaccent(db.Place.name)).like(func.lower(db.f_unaccent(f'%{qw}%'))))
+            if place.level > 0:
+                place_q = place_q.join(db.RegionDescendant, and_(db.RegionDescendant.region == place.place_id,
+                                                                 db.RegionDescendant.descendant == db.Place.place_id))
+            found_places = (place_q
+                            .options(joinedload(db.Place.parent_place))
+                            .order_by(db.Place.level, db.Place.name, db.Place.place_id)
+                            .limit(max_places)
+                            .all())
+
+            if not found_places:
+                search_failed = True
+            if len(found_places) == 1:
+                flash('Nalezeno toto místo', 'info')
+                return redirect(url_for('org_place', id=found_places[0].place_id))
+            else:
+                search_limited = len(found_places) >= max_places
+
     if place.type == db.PlaceType.school:
         school = sess.query(db.School).get(place.place_id)
     else:
@@ -33,19 +77,13 @@ def org_place(id: int):
     children = sorted(place.children, key=lambda p: locale.strxfrm(p.name))
     rr = g.gatekeeper.rights_for(place)
 
-    contests = (sess.query(db.Contest)
-                    .options(joinedload(db.Contest.round))
-                    .filter_by(place=place)
-                    .all())
-
-    contests.sort(key=lambda c: (-c.round.year, c.round.category, c.round.seq, c.round.part))
-
     return render_template(
         'org_place.html', place=place, school=school,
         can_edit=rr.can_edit_place(place),
         can_add_child=rr.can_add_place_child(place),
         children=children,
-        contests=contests
+        search_form=search_form,
+        found_places=found_places, search_failed=search_failed, search_limited=search_limited,
     )
 
 
@@ -232,7 +270,7 @@ def org_place_move(id: int):
         if not rr.can_edit_place(new_parent):
             search_error = 'Nemáte právo k editaci vybraného nadřazeného místa, přesun nelze uskutečnit'
         elif (new_parent.level + 1) not in levels:
-            search_error = f'Toto místo ({place.type_name()}) nelze přemístit pod vybrané místo ({new_parent.type_name()}), dostalo by se na nepovolený level'
+            search_error = f'Toto místo ({place.type_name()}) nelze přemístit pod vybrané místo ({new_parent.type_name()}), dostalo by se na nepovolenou úroveň'
         elif new_parent.place_id == place.parent:
             search_error = 'Žádná změna, místo je zde již umístěno'
         elif form.move.data:
@@ -372,8 +410,8 @@ def org_place_root():
     return redirect(url_for('org_place', id=root.place_id))
 
 
-@app.route('/org/place/<int:id>/rights')
-def org_place_rights(id: int):
+@app.route('/org/place/<int:id>/roles')
+def org_place_roles(id: int):
     sess = db.get_session()
 
     place = sess.query(db.Place).get(id)
@@ -383,14 +421,38 @@ def org_place_rights(id: int):
     parent_ids = [p.place_id for p in g.gatekeeper.get_ancestors(place)]
     roles = (sess.query(db.UserRole)
              .filter(db.UserRole.place_id.in_(parent_ids))
-             .options(joinedload(db.UserRole.user))
+             .options(joinedload(db.UserRole.user),
+                      joinedload(db.UserRole.place),
+                      joinedload(db.UserRole.assigned_by_user))
              .all())
     roles.sort(key=lambda r: (mo.rights.role_order_by_type[r.role], r.user.sort_key()))
 
+    assigned_roles = [r for r in roles if r.place_id == id]
+    inherited_roles = [r for r in roles if r.place_id != id]
+
     rr = g.gatekeeper.rights_for(place)
     rights = sorted(rr.rights, key=lambda r: r.name)
 
     return render_template(
-        'org_place_rights.html', place=place, rights=rights,
-        roles=roles, roles_by_type=mo.rights.roles_by_type
+        'org_place_roles.html', place=place, rights=rights,
+        assigned_roles=assigned_roles, inherited_roles=inherited_roles,
+        roles_by_type=mo.rights.roles_by_type,
     )
+
+
+@app.route('/org/place/<int:id>/contests')
+def org_place_contests(id: int):
+    sess = db.get_session()
+
+    place = sess.query(db.Place).get(id)
+    if not place:
+        raise werkzeug.exceptions.NotFound()
+
+    contests = (sess.query(db.Contest)
+                    .options(joinedload(db.Contest.round))
+                    .filter_by(place=place)
+                    .all())
+
+    contests.sort(key=lambda c: (-c.round.year, c.round.category, c.round.seq, c.round.part))
+
+    return render_template('org_place_contests.html', place=place, contests=contests)
diff --git a/mo/web/templates/org_index.html b/mo/web/templates/org_index.html
index a5962c432b06e1753b9f6c039ab227b24e6365ae..177a3b86edeed21a35c905d7e4c7f1115002cf2b 100644
--- a/mo/web/templates/org_index.html
+++ b/mo/web/templates/org_index.html
@@ -80,17 +80,15 @@
 
 {% endif %}
 
-<h3>Rychlé hledání</h3>
-
-<form method=GET action="" class='form form-inline' role=form>
-	<input class='form-control' name=place placeholder='Kód místa'></input>
-	<input class='btn btn-primary' type="submit" value='Vyhledat'>
-</form>
 {% if g.user.is_admin %}
+
+<h3>Univerzální hledátko</h3>
+
 <form method=GET action="" class='form form-inline' role=form>
-	<input class='form-control' name=uid placeholder='ID uživatele'></input>
+	<input class='form-control' name=search placeholder='cID pID rID uID' autofocus></input>
 	<input class='btn btn-primary' type="submit" value='Vyhledat'>
 </form>
+
 {% endif %}
 
 {% endblock %}
diff --git a/mo/web/templates/org_place.html b/mo/web/templates/org_place.html
index d7b4a0e0a94fba10369d661eaf50d6a05ecb298b..14d21ac3a52d75a76c1be715a9a032c0ccbf4810 100644
--- a/mo/web/templates/org_place.html
+++ b/mo/web/templates/org_place.html
@@ -1,4 +1,5 @@
 {% extends "base.html" %}
+{% import "bootstrap/wtf.html" as wtf %}
 {% block title %}{{ place.type_name().title() }}: {{ place.name }}{% endblock %}
 {% block breadcrumbs %}
 {{ place_breadcrumbs(place) }}
@@ -28,12 +29,48 @@
 		</form>
 		<a class="btn btn-default" href="{{ url_for('org_place_move', id=place.place_id) }}">Přesunout</a>
 {% endif %}
-		<a class="btn btn-default" href='{{ url_for('org_place_rights', id=place.place_id) }}'>Přístupová práva</a>
+		<a class="btn btn-default" href='{{ url_for('org_place_contests', id=place.place_id) }}'>Soutěže</a>
+		<a class="btn btn-default" href='{{ url_for('org_place_roles', id=place.place_id) }}'>Organizátoři</a>
 {% if g.user.is_admin %}
 		<a class="btn btn-default" href="{{ log_url('place', place.place_id) }}">Historie</a>
 {% endif %}
 	</div>
 
+<h3>Vyhledávání míst{% if place.level > 0 %} v této oblasti{% endif %}</h3>
+
+{% if search_failed %}
+<div class='alert alert-danger' role='alert'>
+	Žádné vyhovující místo nenalezeno.
+</div>
+{% endif %}
+
+{% if search_limited %}
+<div class='alert alert-warning' role='alert'>
+	Nalezeno příliš mnoho míst, zobrazeno jen prvních {{ found_places|length }}.
+</div>
+{% endif %}
+
+{{ wtf.quick_form(search_form, method='GET', form_type='inline', button_map={'submit': 'primary'}) }}
+
+{% if found_places %}
+	<table class=data>
+	<thead><tr>
+		<th>Kód
+		<th>Typ
+		<th>Název
+	</thead>
+	{% for p in found_places %}
+	<tr>
+		<td>{{ p.get_code() }}
+		<td>{{ p.type_name() }}
+		<td><a href='{{ url_for('org_place', id=p.place_id) }}'>{{ p.name_or_id() }}</a>
+			{% if p.parent_place.level > 0 %}
+				({{ p.parent_place.type_name() }} {{ p.parent_place.name_or_id() }})
+			{% endif %}
+	{% endfor %}
+	</table>
+{% endif %}
+
 {% if place.can_have_child() %}
 	<h3>Podřízená místa</h3>
 {% if children %}
@@ -41,14 +78,14 @@
 	<table class=data>
 	<thead><tr>
 		<th>Kód
-		<th>Název
 		<th>Typ
+		<th>Název
 	</thead>
 	{% for child in children %}
 	<tr>
 		<td>{{ child.get_code() }}
-		<td><a href='{{ url_for('org_place', id=child.place_id) }}'>{{ child.name or "#" + child.place_id|string }}</a>
 		<td>{{ child.type_name() }}
+		<td><a href='{{ url_for('org_place', id=child.place_id) }}'>{{ child.name_or_id() }}</a>
 	{% endfor %}
 	</table>
 {% endif %}
@@ -57,28 +94,4 @@
 {% endif %}
 {% endif %}
 
-<h3>Soutěže</h3>
-
-{% if not contests %}
-<p>K tomuto místu nejsou přidružené žádné soutěže.
-{% else %}
-<table class=data>
-		<thead><tr>
-			<th>ID
-			<th>Ročník
-			<th>Kat.
-			<th>Název
-			<th>Stav
-		</thead>
-	{% for c in contests %}
-		<tr>
-			{% set r = c.round %}
-			<td><a href='{{ url_for('org_contest', ct_id=c.contest_id) }}'>{{ r.round_code() }}</a>
-			<td>{{ r.year }}
-			<td>{{ r.category }}
-			<td>{{ r.name }}
-			<td class='rstate-{{c.state.name}}'>{{ c.state.friendly_name() }}
-	{% endfor %}
-</table>
-{% endif %}
 {% endblock %}
diff --git a/mo/web/templates/org_place_contests.html b/mo/web/templates/org_place_contests.html
new file mode 100644
index 0000000000000000000000000000000000000000..00af0b43f4471235132a94c37bfa1731e9d3980c
--- /dev/null
+++ b/mo/web/templates/org_place_contests.html
@@ -0,0 +1,32 @@
+{% extends "base.html" %}
+{% block title %}{{ place.type_name().title() }}: {{ place.name }} &ndash; soutěže{% endblock %}
+{% block breadcrumbs %}
+{{ place_breadcrumbs(place, action="Soutěže") }}
+{% endblock %}
+
+{% block body %}
+
+{% if not contests %}
+<p>V tomto místě se nekonají žádné soutěže.
+{% else %}
+<table class=data>
+		<thead><tr>
+			<th>ID
+			<th>Ročník
+			<th>Kat.
+			<th>Název
+			<th>Stav
+		</thead>
+	{% for c in contests %}
+		<tr>
+			{% set r = c.round %}
+			<td><a href='{{ url_for('org_contest', ct_id=c.contest_id) }}'>{{ r.round_code() }}</a>
+			<td>{{ r.year }}
+			<td>{{ r.category }}
+			<td>{{ r.name }}
+			<td class='rstate-{{c.state.name}}'>{{ c.state.friendly_name() }}
+	{% endfor %}
+</table>
+{% endif %}
+
+{% endblock %}
diff --git a/mo/web/templates/org_place_rights.html b/mo/web/templates/org_place_rights.html
deleted file mode 100644
index 22505de047e20e161449d80c7fefc3007b757205..0000000000000000000000000000000000000000
--- a/mo/web/templates/org_place_rights.html
+++ /dev/null
@@ -1,40 +0,0 @@
-{% extends "base.html" %}
-{% block title %}{{ place.type_name().title() }}: {{ place.name }} &ndash; role{% endblock %}
-{% block breadcrumbs %}
-{{ place_breadcrumbs(place, action="Role") }}
-{% endblock %}
-
-{% block body %}
-	<table class=data>
-	<thead><tr>
-		<th>Role
-		<th>Jméno
-		<th>Roč.
-		<th>Kat.
-		<th class='has-tip' title='Pořadí kola v kategorii'>Kolo
-		<th>Zdroj
-	</thead>
-	{% for role in roles %}
-	<tr>
-		<td>{{ roles_by_type[role.role].name }}
-		<td>{{ role.user|user_link }}
-		<td>{{ role.year or '–' }}
-		<td>{{ role.category or '–' }}
-		<td>{{ role.seq or '–' }}
-		<td>{% if role.place_id == place.place_id %}přiděleno{% else %}<a href='{{ url_for('org_place_rights', id=role.place_id) }}'>zděděno</a>{% endif %}
-	{% endfor %}
-	</table>
-
-<!--
-Odvozená práva:
-{% if g.user.is_admin %}
-	admin
-{% elif rights %}
-	{% for r in rights %}
-	{{ r.name }}
-	{% endfor %}
-{% else %}
-	žádná
-{% endif %}
--->
-{% endblock %}
diff --git a/mo/web/templates/org_place_roles.html b/mo/web/templates/org_place_roles.html
new file mode 100644
index 0000000000000000000000000000000000000000..dfaaa739eebd66b05b367598e5a7c035fbdbeb64
--- /dev/null
+++ b/mo/web/templates/org_place_roles.html
@@ -0,0 +1,58 @@
+{% extends "base.html" %}
+{% block title %}{{ place.type_name().title() }}: {{ place.name }} &ndash; organizátoři{% endblock %}
+{% block breadcrumbs %}
+{{ place_breadcrumbs(place, action="Organizátoři") }}
+{% endblock %}
+
+{% macro show_roles(title, roles, inherited) %}
+	<h3>{{ title }}</h3>
+	{% if roles %}
+		<table class=data>
+		<thead><tr>
+			<th>Role
+			<th>Jméno
+			<th>Roč.
+			<th>Kat.
+			<th class='has-tip' title='Pořadí kola v kategorii'>Kolo
+			{% if inherited %}
+			<th>Zděděno z
+			{% else %}
+			<th>Přidělil
+			{% endif %}
+		</thead>
+		{% for role in roles %}
+		<tr>
+			<td>{{ roles_by_type[role.role].name }}
+			<td>{{ role.user|user_link }}
+			<td>{{ role.year or '–' }}
+			<td>{{ role.category or '–' }}
+			<td>{{ role.seq or '–' }}
+			{% if inherited %}
+			<td><a href='{{ url_for('org_place_roles', id=role.place_id) }}'>{{ role.place.type_name() }} {{ role.place.name_or_id() }}</a>
+			{% else %}
+			<td>{% if role.assigned_by_user %}{{ role.assigned_by_user|user_link }}{% else %}systém{% endif %}
+			{% endif %}
+		{% endfor %}
+		</table>
+	{% else %}
+		<em>Žádné.</em>
+	{% endif %}
+{% endmacro %}
+
+{% block body %}
+{{ show_roles("Přidělené role", assigned_roles, False) }}
+{{ show_roles("Zděděné role", inherited_roles, True) }}
+
+<!--
+Odvozená práva:
+{% if g.user.is_admin %}
+	admin
+{% elif rights %}
+	{% for r in rights %}
+	{{ r.name }}
+	{% endfor %}
+{% else %}
+	žádná
+{% endif %}
+-->
+{% endblock %}