function seekToOp(docState, ops) {
  const { objActor, objActorNum, objCtr, keyActor, keyCtr, keyStr } = ops
  let blockIndex = 0, totalVisible = 0

  // Skip any blocks that contain only objects with lower objectIds
  if (objCtr !== null) {
    while (blockIndex < docState.blocks.length - 1) {
      const blockActor = docState.blocks[blockIndex].lastObjectActor === undefined ? undefined
        : docState.actorIds[docState.blocks[blockIndex].lastObjectActor]
      const blockCtr = docState.blocks[blockIndex].lastObjectCtr
      if (blockCtr === null || blockCtr < objCtr || (blockCtr === objCtr && blockActor < objActor)) {
        blockIndex++
      } else {
        break
      }
    }
  }

  if (keyStr !== null) {
    // String key is used. First skip any blocks that contain only lower keys
    while (blockIndex < docState.blocks.length - 1) {
      const { lastObjectActor, lastObjectCtr, lastKey } = docState.blocks[blockIndex]
      if (objCtr === lastObjectCtr && objActorNum === lastObjectActor &&
          lastKey !== undefined && lastKey < keyStr) blockIndex++; else break
    }

    // When we have a candidate block, decode it to find the exact insertion position
    const {skipCount} = seekWithinBlock(ops, docState.blocks[blockIndex].columns, docState.actorIds, false)
    return {blockIndex, skipCount, visibleCount: 0}

  } else {
    // List operation
    const insertAtHead = keyCtr === null || keyCtr === 0 || keyActor === null
    const keyActorNum = keyActor === null ? null : docState.actorIds.indexOf(keyActor)
    let resumeInsertion = false

    while (true) {
      // Search for the reference element, skipping any blocks whose Bloom filter does not contain
      // the reference element. We only do this if not inserting at the head (in which case there is
      // no reference element), or if we already found the reference element in an earlier block (in
      // which case we have resumeInsertion === true). The latter case arises with concurrent
      // insertions at the same position, and so we have to scan beyond the reference element to
      // find the actual insertion position, and that further scan crosses a block boundary.
      if (!insertAtHead && !resumeInsertion) {
        while (blockIndex < docState.blocks.length - 1 &&
               docState.blocks[blockIndex].lastObjectActor === objActorNum &&
               docState.blocks[blockIndex].lastObjectCtr === objCtr &&
               !bloomFilterContains(docState.blocks[blockIndex].bloom, keyActorNum, keyCtr)) {
          // If we reach the end of the list object without a Bloom filter hit, the reference element
          // doesn't exist
          if (docState.blocks[blockIndex].lastObjectCtr > objCtr) {
            throw new RangeError(`Reference element not found: ${keyCtr}@${keyActor}`)
          }

          // Add up number of visible list elements in any blocks we skip, for list index computation
          totalVisible += visibleListElements(docState, blockIndex, objActorNum, objCtr)
          blockIndex++
        }
      }

      // We have a candidate block. Decode it to see whether it really contains the reference element
      const {found, skipCount, visibleCount} = seekWithinBlock(ops,
                                                               docState.blocks[blockIndex].columns,
                                                               docState.actorIds,
                                                               resumeInsertion)

      if (blockIndex === docState.blocks.length - 1 ||
          docState.blocks[blockIndex].lastObjectActor !== objActorNum ||
          docState.blocks[blockIndex].lastObjectCtr !== objCtr) {
        // Last block: if we haven't found the reference element by now, it's an error
        if (found) {
          return {blockIndex, skipCount, visibleCount: totalVisible + visibleCount}
        } else {
          throw new RangeError(`Reference element not found: ${keyCtr}@${keyActor}`)
        }

      } else if (found && skipCount < docState.blocks[blockIndex].numOps) {
        // The insertion position lies within the current block
        return {blockIndex, skipCount, visibleCount: totalVisible + visibleCount}
      }

      // Reference element not found and there are still blocks left ==> it was probably a false positive.
      // Reference element found, but we skipped all the way to the end of the block ==> we need to
      // continue scanning the next block to find the actual insertion position.
      // Either way, go back round the loop again to skip blocks until the next Bloom filter hit.
      resumeInsertion = found && ops.insert
      totalVisible += visibleListElements(docState, blockIndex, objActorNum, objCtr)
      blockIndex++
    }
  }
}
Scans the blocks of document operations to find the position where a new operation should be
inserted. Returns an object with keys:
- `blockIndex`: the index of the block into which we should insert the new operation
- `skipCount`: the number of operations, counted from the start of the block, after which the
  new operations should be inserted or merged.
- `visibleCount`: if modifying a list object, the number of visible (i.e. non-deleted) list
  elements that precede the position where the new operations should be applied.